Потребление Polymorphi c Jsons с модификацией и Kotlin - PullRequest
0 голосов
/ 16 апреля 2020

Мой API отправляет мне полифонию c 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
            }]
        }]
    }
})

Вот пользовательский десерализатор , который включает в себя 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 Ответ

0 голосов
/ 17 апреля 2020

Я попытался это сделать и выдвинул две возможные идеи. Я не думаю, что они являются единственным способом достичь этого, но я думаю, что могу поделиться своими мыслями.

Во-первых, я сократил проблему до фактического разбора элементов. Поэтому я удалил модификацию из уравнения и использую следующие 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 как обобщенного c JsonElement:

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

(я использую классы данных, потому что Я нахожу их более полезными, но у вас их тоже нет)

Идея здесь в том, чтобы позволить gson десериализовать его как универсальный c 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())
}

Я думаю, что наибольшим преимуществом этого является то, что он довольно прост и вам не требуется настраиваемый лог c для десериализации. Для меня огромным недостатком является потеря безопасности типов.

Вы можете добавить дополнение в виде массива, но это будет массив 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 возможных типа для вашего варианта использования.

Теперь нам нужен custom deserializer:

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*.

Надеюсь, это поможет

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...