0

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?

F.P. Lucas
  • 21
  • 4

2 Answers2

1

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())
        }
    })
}
F.P. Lucas
  • 21
  • 4
1

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.

Marcono1234
  • 5,856
  • 1
  • 25
  • 43