【问题标题】:Consuming Polymorphic Jsons with Retrofit and Kotlin使用 Retrofit 和 Kotlin 使用多态 Json
【发布时间】:2020-07-29 02:43:21
【问题描述】:

我的 API 向我发送了一个复音 Json,其中变量 addon_item 可以是字符串或数组,我花了几天时间尝试为其创建 CustomDezerializer,但没有任何成功。

这是 Json 响应

({
    "code": 1,
    "msg": "OK",
    "details": {
        "merchant_id": "62",
        "item_id": "1665",
        "item_name": "Burrito",
        "item_description": "Delicioso Burrito en base de tortilla de 30 cm",
        "discount": "",
        "photo": "http:\/\/www.asiderapido.cloud\/upload\/1568249379-KDKQ5789.jpg",
        "item_cant": "-1",
        "cooking_ref": false,
        "cooking_ref_trans": "",
        "addon_item": [{
            "subcat_id": "144",
            "subcat_name": "EXTRA",
            "subcat_name_trans": "",
            "multi_option": "multiple",
            "multi_option_val": "",
            "two_flavor_position": "",
            "require_addons": "",
            "sub_item": [{
                "sub_item_id": "697",
                "sub_item_name": "Queso cheddar",
                "item_description": "Delicioso queso fundido",
                "price": "36331.20",
                "price_usd": null
            }]
        }]
    }
})

这里是 Custom Dezerializer,其中包括 BodyConverter,它删除了包含 Json 响应的两个大括号:

'''
/**
 * This class was created due to 2 issues with the current API responses:
 * 1. The API JSON results where encapsulated by parenthesis
 * 2. They had dynamic JSON variables, where the Details variable was coming as a String
 * or as an Object depending on the error message (werer whe user and password wereh correct.
 *
 */

class JsonConverter(private val gson: Gson) : Converter.Factory() {

    override fun responseBodyConverter(
        type: Type?, annotations: Array<Annotation>?,

        retrofit: Retrofit?
    ): Converter<ResponseBody, *>? {
        val adapter = gson.getAdapter(TypeToken.get(type!!))
        return GsonResponseBodyConverter(gson, adapter)
    }

    override fun requestBodyConverter(
        type: Type?,
        parameterAnnotations: Array<Annotation>?,
        methodAnnotations: Array<Annotation>?,
        retrofit: Retrofit?
    ): Converter<*, RequestBody>? {
        val adapter = gson.getAdapter(TypeToken.get(type!!))
        return GsonRequestBodyConverter(gson, adapter)
    }


    internal inner class GsonRequestBodyConverter<T>(
        private val gson: Gson,
        private val adapter: TypeAdapter<T>
    ) : Converter<T, RequestBody> {
        private val MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8")
        private val UTF_8 = Charset.forName("UTF-8")

        @Throws(IOException::class)
        override fun convert(value: T): RequestBody {
            val buffer = Buffer()
            val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
            val jsonWriter = gson.newJsonWriter(writer)
            adapter.write(jsonWriter, value)
            jsonWriter.close()
            return RequestBody.create(MEDIA_TYPE, buffer.readByteString())
        }
    }


    // Here we remove the parenthesis from the JSON response

    internal inner class GsonResponseBodyConverter<T>(
        gson: Gson,
        private val adapter: TypeAdapter<T>
    ) : Converter<ResponseBody, T> {

        @Throws(IOException::class)
        override fun convert(value: ResponseBody): T? {
            val dirty = value.string()
            val clean = dirty.replace("(", "")
                .replace(")", "")

            try {
                return adapter.fromJson(clean)
            } finally {
                value.close()
            }
        }
    }


    class DetalleDeProductoDeserializer : JsonDeserializer<DetallesDelItemWrapper2> {
        override fun deserialize(
            json: JsonElement,
            typeOfT: Type,
            context: JsonDeserializationContext
        ): DetallesDelItemWrapper2 {

             if ((json as JsonObject).get("addon_item") is JsonObject) {
            return Gson().fromJson<DetallesDelItemWrapper2>(json, ListaDetalleAddonItem::class.java)

            } else {

                 return Gson().fromJson<DetallesDelItemWrapper2>(json, DetallesDelItemWrapper2.CookingRefItemBoolean::class.java)
            }
        }
    }

    companion object {

        private val LOG_TAG = JsonConverter::class.java!!.getSimpleName()

        fun create(detalleDeProductoDeserializer: DetalleDeProductoDeserializer): JsonConverter {
            Log.e("Perfill Adapter = ", "Test5 " +  "JsonConverter" )

            return create(Gson())
        }


        fun create(): JsonConverter {
            return create(Gson())
        }


        private fun create(gson: Gson?): JsonConverter {
            if (gson == null) throw NullPointerException("gson == null")
            return JsonConverter(gson)
        }
    }
}

这是 RetrofitClient.class

class RetrofitClient private constructor(name: String) {
    private var retrofit: Retrofit? = null

    fun getApi(): Api {
        return retrofit!!.create(Api::class.java)
    }

    init {

        if (name == "detalleDelItem") run {
            retrofit = Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(JsonConverterJava.create(JsonConverterJava.DetallesDelItemDeserializer()))
//                .addConverterFactory(GsonConverterFactory.create(percentDeserializer))
                .client(unsafeOkHttpClient.build())
                .build()
            Log.e("RetrofitClient ", "Instace: " + "detalle " +  name)
        }
    }

    companion object {

        //Remember this shit is https for the production server
        private val BASE_URL = "http://www.asiderapido.cloud/mobileapp/api/"

        private var mInstance: RetrofitClient? = null

        @Synchronized
        fun getInstance(name: String): RetrofitClient {
                mInstance = RetrofitClient(name)
            return mInstance!!
        }
    }
}

最后是我的POJO

open class DetallesDelItemWrapper2 {
     @SerializedName("code")
     val code: Int? = null
     @Expose
     @SerializedName("details")
     var details: ItemDetails? = null
     @SerializedName("msg")
     val msg: String? = null


     class ItemDetails {
         @Expose
         @SerializedName("addon_item")
         val addonItem: Any? = null
         @SerializedName("category_info")
         val categoryInfo: CategoryInfo? = null
         @SerializedName("cooking_ref")
         val cookingRef: Any? = null
         @SerializedName("cooking_ref_trans")
         val cookingRefTrans: String? = null
     }

class ListaDetalleAddonItem: DetallesDelItemWrapper2(){
   @SerializedName("addon_item")
   val detalleAddonItem: List<DetalleAddonItem>? = null

}

class StringDetalleAddonItem: DetallesDelItemWrapper2(){
    @SerializedName("addon_item")
    val detalleAddonItem: String? = null
}

【问题讨论】:

    标签: android json kotlin retrofit2


    【解决方案1】:

    我对此进行了尝试,并提出了 2 个可能的想法。我不认为它们是实现这一目标的唯一方法,但我想我可以分享我的想法。

    首先,我将问题简化为实际上只解析项目。所以我从等式中删除了改造并使用以下 jsons:

    val json = """{
        "addon_item": [{
                "subcat_id": "144",
                "subcat_name": "EXTRA",
                "subcat_name_trans": "",
                "multi_option": "multiple",
                "multi_option_val": "",
                "two_flavor_position": "",
                "require_addons": "",
                "sub_item": [{
                    "sub_item_id": "697",
                    "sub_item_name": "Queso cheddar",
                    "item_description": "Delicioso queso fundido",
                    "price": "36331.20",
                    "price_usd": null
                }]
            }]
    }
    """.trimIndent()
    

    (当addon_item 是一个数组时)

    val jsonString = """{
       "addon_item": "foo"
    }
    """.trimIndent()
    

    (当addon_item 是一个字符串时)


    第一种方法

    我的第一个方法是将addon_item 建模为通用JsonElement

    data class ItemDetails(
      @Expose
      @SerializedName("addon_item")
      val addonItem: JsonElement? = null
    ) 
    

    (我使用数据类是因为我发现它们更有帮助,但你也没有)

    这里的想法是让gson 将其反序列化为通用 json 元素,然后您可以自己检查它。所以如果我们在类中添加一些方便的方法:

    data class ItemDetails(
      @Expose
      @SerializedName("addon_item")
      val addonItem: JsonElement? = null
    ) {
      fun isAddOnItemString() =
        addonItem?.isJsonPrimitive == true && addonItem.asJsonPrimitive.isString
    
      fun isAddOnItemArray() =
        addonItem?.isJsonArray == true
    
      fun addOnItemAsString() =
        addonItem?.asString
    
      fun addOnItemAsArray() =
        addonItem?.asJsonArray
    }
    

    如您所见,我们检查addOnItem 中包含的内容,并据此获取其内容。这是一个如何使用它的示例:

    fun main() {
      val item = Gson().fromJson(jsonString, ItemDetails::class.java)
      println(item.isAddOnItemArray())
      println(item.isAddOnItemString())
      println(item.addOnItemAsString())
    }
    

    我认为这样做的最大优点是它相当简单,并且您不需要自定义逻辑来反序列化。对我来说,最大的缺点是类型安全损失。

    您可以将插件作为一个数组获取,但它将是一个必须“手动”反序列化的 json 元素数组。因此,我的第二种方法试图解决这个问题。


    第二种方法

    这里的想法是使用 Kotlin 的密封类并有 2 种类型的附加组件:

    sealed class AddOnItems {
      data class StringAddOnItems(
        val addOn: String
      ) : AddOnItems()
    
      data class ArrayAddOnItems(
        val addOns: List<SubCategory> = emptyList()
      ) : AddOnItems()
    
      fun isArray() = this is ArrayAddOnItems
    
      fun isString() = this is StringAddOnItems
    }
    

    SubCategory 类就是列表中的内容。这是它的一个简单版本:

    data class SubCategory(
      @SerializedName("subcat_id")
      val id: String
    )
    

    如您所见,AddOnItems 是一个密封类,对于您的用例只有 2 种可能的类型。

    现在我们需要一个自定义的反序列化器:

    class AddOnItemsDeserializer : JsonDeserializer<AddOnItems> {
      override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?) =
        when {
            json?.isJsonArray == true -> {
                AddOnItems.ArrayAddOnItems(context!!.deserialize(
                    json.asJsonArray,
                    TypeToken.getParameterized(List::class.java, SubCategory::class.java).type))
            }
    
            json?.isJsonPrimitive == true && json.asJsonPrimitive.isString ->
                AddOnItems.StringAddOnItems(json.asJsonPrimitive.asString)
    
            else -> throw IllegalStateException("Cannot parse $json as addonItems")
        }
    }
    

    简而言之,这会检查 add on 是否是一个数组,并创建相应的类和字符串。

    你可以这样使用它:

    fun main() {
      val item = GsonBuilder()
        .registerTypeAdapter(AddOnItems::class.java, AddOnItemsDeserializer())
        .create()
        .fromJson(jsonString, ItemDetails::class.java)
      println(item.addOnItems.isString())
      println(item.addOnItemsAsString().addOn)
    
    
      val item = GsonBuilder()
        .registerTypeAdapter(AddOnItems::class.java, AddOnItemsDeserializer())
        .create()
        .fromJson(json, ItemDetails::class.java)
      println(item.addOnItems.isArray())
      println(item.addOnItemsAsArray().addOns[0])
    }
    

    我认为这里最大的优势是您可以保留类型。但是,在调用addOnItemsAs*之前,您仍然需要检查它是什么。

    希望对你有帮助

    【讨论】:

      猜你喜欢
      • 2021-12-30
      • 2022-01-03
      • 2023-03-11
      • 1970-01-01
      • 2017-11-20
      • 1970-01-01
      • 2016-02-14
      • 2020-04-15
      • 1970-01-01
      相关资源
      最近更新 更多