使用 Retrofit 和 Kotlin 使用多态 Json

Posted

技术标签:

【中文标题】使用 Retrofit 和 Kotlin 使用多态 Json【英文标题】:Consuming Polymorphic Jsons with Retrofit and Kotlin 【发布时间】: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

【问题讨论】:

【参考方案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*之前,您仍然需要检查它是什么。

希望对你有帮助

【讨论】:

以上是关于使用 Retrofit 和 Kotlin 使用多态 Json的主要内容,如果未能解决你的问题,请参考以下文章

使用 Kotlin 进行 RxJava 和改造

在 Kotlin 中使用 Moshi 和 Retrofit 解析具有增量对象名称的 JSON

使用 Retrofit2(kotlin) 的 CURL 请求

使用 Retrofit 处理 Kotlin 序列化 MissingFieldException

如何使用 Kotlin Coroutines 在 Retrofit 中处理 204 响应?

使用 Corrutines 的 Kotlin retrofit2 连接