Home > Software engineering >  Is it possible to treat BEGIN_ARRAY AND BEGIN_OBJECT with retrofit2 for the same response?
Is it possible to treat BEGIN_ARRAY AND BEGIN_OBJECT with retrofit2 for the same response?

Time:09-02

first of all, sorry for the bad english. I'm having a little issue with the app for my company, I started learning Kotlin a few months ago, so it's everything pretty new to me, i did a little digging for most of my problems but this one I didn't find anywhere. We have a server provind data with a Joomla API, the problem is, when I use retrofit2 to get the data with a query, it is possible to return a BEGIN_OBJECT when no data is found, and a BEGIN_ARRAY when the data is found. I found a lot of places telling when it's one but is expected another, but the two in the same response, not yet.

This is the response when the data is not found:

{"err_msg":"Produto n\u00e3o encontrado (CALOTA CORSA)","err_code":404,"response_id":"","api":"","version":"1.0","data":{}}

I called the data class for this piece ProductList, and the data I called Product, for future reference...

This is the response when data is found:

{"err_msg":"","err_code":"","response_id":522,"api":"app.produto","version":"1.0","data":[{"codigo":"0340008","filial":"CPS","referencia":"7898314110118","ncm":"38249941","codigosecundario":"146","nome":"WHITE LUB SUPER AEROSSOL 300ML 146","similar":"0012861","parceiro":"","produtosrelacionados":"0012861;0125121;0125945;0340008;0340035;0340169;0343394;0582033;0582954;0610250;1203682;1227480;1227569;1366196;1366761;1450241;1450861","marca":"ORBI QUIMICA","linha":"DESENGRIPANTE","lancamento":"2011-07-28 00:00:00","quantidadeembalagem":"12","unidademedida":"PC","caracteristicas":"OLEO WHITE LUB SUPER AEROSSOL 300ML - DESENGRIPANTE\/ LUBRIFICANTE\/ PROTETIVO 146","lado":"N\/A","ultima_atualizacao_preco":"2022-08-05 10:32:53","valor":"9.99","ultima_atualizacao_estoque":"2022-09-01 00:03:17","estoque":"200"}]}

When the response is successful, it is possible to recieve up to 10 different products.

This is my retrofit call at the ViewModel, I'm using GsonConverterFactory with retrofit.

fun getProductByCode(code: String, token: String) {

    RetrofitInstance.chgApi.listProducts(code, token).enqueue(object : Callback<ProductList> {
        override fun onResponse(call: Call<ProductList>, response: Response<ProductList>) {
            if (response.body()?.errCode != "") {
                Log.e("Response", response.body()?.errMsg!!)
                errMsg.value = response.body()?.errMsg!!
            } else {
                errMsg.value = ""
                products.value = response.body()!!.data
            }
        }

        override fun onFailure(call: Call<ProductList>, t: Throwable) {
            Log.e("Error", t.message.toString())
        }
    })
}

First data class

data class ProductList(
    @SerializedName("err_msg") var errMsg : String,
    @SerializedName("err_code") var errCode : String,
    @SerializedName("response_id") var responseId : String,
    @SerializedName("api") var api : String,
    @SerializedName("version") var version : String,
    @SerializedName("data") var data: ArrayList<Product>
)

Second data class

@Entity(tableName = PRODUCT_DATABASE)
data class Product(

    @PrimaryKey
    @SerializedName("codigo"                     ) var codigo                   : String,
    @SerializedName("filial"                     ) var filial                   : String,
    @SerializedName("referencia"                 ) var referencia               : String,
    @SerializedName("ncm"                        ) var ncm                      : String,
    @SerializedName("codigosecundario"           ) var codigosecundario         : String,
    @SerializedName("nome"                       ) var nome                     : String,
    @SerializedName("similar"                    ) var similar                  : String,
    @SerializedName("parceiro"                   ) var parceiro                 : String,
    @SerializedName("produtosrelacionados"       ) var produtosrelacionados     : String,
    @SerializedName("marca"                      ) var marca                    : String,
    @SerializedName("linha"                      ) var linha                    : String,
    @SerializedName("lancamento"                 ) var lancamento               : String,
    @SerializedName("quantidadeembalagem"        ) var quantidadeembalagem      : String,
    @SerializedName("unidademedida"              ) var unidademedida            : String,
    @SerializedName("caracteristicas"            ) var caracteristicas          : String,
    @SerializedName("lado"                       ) var lado                     : String,
    @SerializedName("ultima_atualizacao_preco"   ) var ultimaAtualizacaoPreco   : String,
    @SerializedName("valor"                      ) var valor                    : String,
    @SerializedName("ultima_atualizacao_estoque" ) var ultimaAtualizacaoEstoque : String,
    @SerializedName("estoque"                    ) var estoque                  : String,
    var cesta : Int,
    var comprar : Boolean

)

The simple way to treat would be to change my data class, changing the type of the field "data" to ArrayList< Product > or to only Product, but, as far as I know, it can't be both at the same time... Any suggestions?

CodePudding user response:

Long story short, it took me the whole day and I found a solution here:

how to handle two different Retrofit response in Kotlin?

Just changing my Callback, Call and Response to < Any >, creating a new model and doing some treatment acording to the response. The final code:

fun searchProduct(code: String, token: String) {

    RetrofitInstance.chgApi.listProducts(code.uppercase(), token).enqueue(object : Callback<Any> {

        override fun onResponse(call: Call<Any>, response: Response<Any>) {

            val gson = Gson()

            if (response.body().toString().contains("err_msg=, err_code=")) {
                
                productList = gson.fromJson(gson.toJson(response.body()), ProductList::class.java)
                products.value = productList.data

            } else {

                productListError = gson.fromJson(gson.toJson(response.body()), ProductListError::class.java)
                errMsg.value = productListError.errMsg

            }
        }

        override fun onFailure(call: Call<Any>, t: Throwable) {
            Log.e("Error", t.message.toString())
        }
    })
}

CodePudding user response:

Assuming that, as shown in your answer, you have two separate model classes, one for a successful response and one for an error response, and a common supertype (for example an interface Response), you could solve this with a custom JsonDeserializer 1. It should based on the members and the values of the JsonObject decide as which type the data should be deserialized. This way you can keep data: List<Product> for the ProductList response.

object ProductListResponseDeserializer : JsonDeserializer<Response> {
    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Response {
        val jsonObject = json.asJsonObject

        val errCode = jsonObject.getAsJsonPrimitive("err_code").asString
        val errMsg = jsonObject.getAsJsonPrimitive("err_msg").asString

        val responseType = if (errCode.isEmpty() && errMsg.isEmpty())
            ProductList::class.java
            else ProductListError::class.java

        return context.deserialize(json, responseType)
    }
}

(Note: Instead of duplicating the strings "err_code" and "err_msg" here and in your model classes, you could also use a single constant which is read here and used for the @SerializedName in your model classes.)

You would then have to create a GsonBuilder, register the deserializer and use Retrofit's GsonConverterFactory to use the custom Gson instance:

val gson = GsonBuilder()
    .registerTypeAdapter(Response::class.java, ProductListResponseDeserializer)
    .create()

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(...)
    .addConverterFactory(GsonConverterFactory.create(gson))
    .build();

And in your callback check the class of the Response instance (whether it is a ProductList or a ProductListError).


1: In general TypeAdapter should be preferred over JsonDeserializer because it is more performant, but because here the data needs to be parsed as JsonObject anyway, there is most likely no difference.

  • Related