使用 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 中使用 Moshi 和 Retrofit 解析具有增量对象名称的 JSON
使用 Retrofit2(kotlin) 的 CURL 请求
使用 Retrofit 处理 Kotlin 序列化 MissingFieldException