簡體   English   中英

如何使用 retrofit 和 gson 轉換器將嵌套的 JSON 展平為單個 class?

[英]how to flatten nested JSON into single class using retrofit and gson converter?

我有一個來自服務器的嵌套 JSON,如您所見,位置中有一個嵌套數據

{

    "id": "18941862",
    "name": "Pizza Maru",
    "url": "https://www.zomato.com/jakarta/pizza-maru-1-thamrin?utm_source=api_basic_user&utm_medium=api&utm_campaign=v2.1",
    "location": {
        "address": "Grand Indonesia Mall, East Mall, Lantai 3A, Jl. M.H. Thamrin No. 1, Thamrin, Jakarta",
        "locality": "Grand Indonesia Mall, Thamrin",
        "city": "Jakarta",
        "city_id": 74,
        "latitude": "-6.1954467635",
        "longitude": "106.8216102943",
        "zipcode": "",
        "country_id": 94,
        "locality_verbose": "Grand Indonesia Mall, Thamrin, Jakarta"
    },

    "currency": "IDR"

}

我正在使用 retrofit 並使用 gson 轉換器。 通常我需要制作 2 個數據 class 到 map JSON 到 POJO。 所以我需要制作 Restaurant class 和 Location class,但我需要將 json object 扁平化為單個 Restaurant class,就像這樣

data class Restaurant :  {

    var id: String
    var name: String
    var url: String
    var city: String
    var latitude: Double
    var longitude: Double
    var zipcode: String
    var currency: String 

}

如果我使用 retrofit 和 gson 轉換器,該怎么做?

java 或 kotlin 都可以

此解決方案是解決此問題的靈丹妙葯,值得贊賞。

先拿這個Kotlin文件:

/**
 * credits to https://github.com/Tishka17/gson-flatten for inspiration
 * Author: A$CE
 */

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Flatten(val path: String)

class FlattenTypeAdapterFactory(
    private val pathDelimiter: String = "."
): TypeAdapterFactory {

    override fun <T: Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
        val delegateAdapter = gson.getDelegateAdapter(this, type)
        val defaultAdapter = gson.getAdapter(JsonElement::class.java)
        val flattenedFieldsCache = buildFlattenedFieldsCache(type.rawType)

        return object: TypeAdapter<T>() {

            @Throws(IOException::class)
            override fun read(reader: JsonReader): T {
                // if this class has no flattened fields, parse it with regular adapter
                if(flattenedFieldsCache.isEmpty())
                    return delegateAdapter.read(reader)
                // read the whole json string into a jsonElement
                val rootElement = defaultAdapter.read(reader)
                // if not a json object (array, string, number, etc.), parse it
                if(!rootElement.isJsonObject)
                    return delegateAdapter.fromJsonTree(rootElement)
                // it's a json object of type T, let's deal with it
                val root = rootElement.asJsonObject
                // parse each field
                for(field in flattenedFieldsCache) {
                    var element: JsonElement? = root
                    // dive down the path to find the right element
                    for(node in field.path) {
                        // can't dive down null elements, break
                        if(element == null) break
                        // reassign element to next node down
                        element = when {
                            element.isJsonObject -> element.asJsonObject[node]
                            element.isJsonArray -> try {
                                element.asJsonArray[node.toInt()]
                            } catch(e: Exception) { // NumberFormatException | IndexOutOfBoundsException
                                null
                            }
                            else -> null
                        }
                    }
                    // lift deep element to root element level
                    root.add(field.name, element)
                    // this keeps nested element un-removed (i suppose for speed)
                }
                // now parse flattened json
                return delegateAdapter.fromJsonTree(root)
            }

            override fun write(out: JsonWriter, value: T) {
                throw UnsupportedOperationException()
            }
        }.nullSafe()
    }

    // build a cache for flattened fields's paths and names (reflection happens only here)
    private fun buildFlattenedFieldsCache(root: Class<*>): Array<FlattenedField> {
        // get all flattened fields of this class
        var clazz: Class<*>? = root
        val flattenedFields = ArrayList<Field>()
        while(clazz != null) {
            clazz.declaredFields.filterTo(flattenedFields) {
                it.isAnnotationPresent(Flatten::class.java)
            }
            clazz = clazz.superclass
        }

        if(flattenedFields.isEmpty()) {
            return emptyArray()
        }
        val delimiter = pathDelimiter
        return Array(flattenedFields.size) { i ->
            val ff = flattenedFields[i]
            val a = ff.getAnnotation(Flatten::class.java)!!
            val nodes = a.path.split(delimiter)
                .filterNot { it.isEmpty() } // ignore multiple or trailing dots
                .toTypedArray()
            FlattenedField(ff.name, nodes)
        }
    }

    private class FlattenedField(val name: String, val path: Array<String>)
}

然后像這樣將它添加到Gson

val gson = GsonBuilder()
            .registerTypeAdapterFactory(FlattenTypeAdapterFactory())
            .create()
Retrofit.Builder()
            .baseUrl(baseUrl)
            ...
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build()

使用您的示例,您可以像這樣解析 pojo:

// prefer constructor properties
// prefer val over var
// prefer added @SerializedName annotation even to same-name properties:
// to future proof and for easier proguard rule config
data class Restaurant(
    @SerializedName("id") val id: String,
    @SerializedName("name") val name: String,
    @SerializedName("url") val url: String,
    @Flatten("location.city") val city: String,
    @Flatten("location.latitude") val latitude: Double,
    @Flatten("location.longitude") val longitude: Double,
    @Flatten("location.zipcode") val zipcode: String,
    @SerializedName("currency") var currency: String 
)

您甚至可以在數組中寫下路徑,例如@Flatten("friends.0.name") = 獲取第一個朋友的名字。 有關更多信息,請訪問存儲庫

但是請注意,我將TypeAdapter為僅讀取/使用 json 個對象。 如果你也想用它來寫 json ,你可以實現write()

不客氣。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM