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.