Get all nested JSON objects with GSON: Deserialization - android

[
{
"account":{
"availableBalanceInCents":0.0,
"unitCredits":[
],
"accountId":2001003318,
"currentBalanceInCents":0.0,
"reservations":[],
"accountSummary":{},
"accountHistory":{},
"status":8
}
},
{
"account":{
"availableBalanceInCents":0.0,
"unitCredits":[],
"accountId":2001003318,
"currentBalanceInCents":0.0,
"reservations":[],
"accountSummary":{},
"accountHistory":{},
"status":8
}
},
{},
{}
]
I want to get list of account using gson deserialize. Pls help how can I ignore the empty object.
I've done something like this till now, but it returns the empty objects.
val balance =
GsonBuilder()
.create()
.fromJson<ArrayList<BalanceDetail>>(
JSONArray(data)
.toString(),
object : TypeToken<List<BalanceDetail>>() {}.type
)

You can use wrapper:
data class BalanceDetailWrapper(
#SerializedName("account") val account: BalanceDetail?
)
And deserialize like this:
val type = object : TypeToken<List<BalanceDetailWrapper>>() {}.type
val balance = GsonBuilder().create()
.fromJson<ArrayList<BalanceDetailWrapper>>(JSONArray(data).toString(), type)

Try:
val balance =
GsonBuilder()
.registerTypeHierarchyAdapter(
BalanceDetail::class.java,
object : JsonDeserializer<BalanceDetail?> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): BalanceDetail? {
return if (json?.asJsonObject?.entrySet()?.size == 0) {
null
} else {
Gson().fromJson(json, BalanceDetail::class.java)
}
}
})
.create()
.fromJson<ArrayList<BalanceDetail>>(
JSONArray(value)
.toString(),
object : TypeToken<List<BalanceDetail>>() {}.type
)

In my opinion you need just to filter out null elements with List.filterNotNull() function

Related

How to deserialize JSON response in GSON for inconsistent API response?

I have looked into this issue, and tried to implement what i could find over the SO, but no luck. So is my problem:
Sometimes, in my JSON response, I get the data field as a JSON object, and other times the data field as JSON array. Technically this is a bad API design, however changing the API at this point is not feasible.
Two types of JSON responses by API
{
data: {
.
.
.
}
}
{
data : [
.
.
.
]
}
I have tried to implement the DataDeserializer, however the GSON still identifies the JSON response as JSON object and not able to use the DataDeserializer:
Class DataDeserializer
class DataDeserializer : JsonDeserializer<List<Data<Any>>> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext
): List<Data<Any>> {
val dataList = ArrayList<Data<Any>>()
when {
json.isJsonObject -> {
val data = context.deserialize<Data<Any>>(json.asJsonObject, Data::class.java)
dataList.add(data)
}
json.isJsonArray -> {
for (jsonObject in json.asJsonArray)
dataList.add(context.deserialize(jsonObject, Data::class.java))
}
else -> throw RuntimeException("Unexpected JSON Type: ${json.javaClass}")
}
return dataList
}
}
Model Classes
open class Json<T> {
lateinit var data: List<Data<T>>
fun getFirstChild() = data.first()
}
data class Data<T>(
private val id: String = "",
private val type: String = "",
val attributes: T
)
Registering DataDeserializer with GSONConverterFactory
val gson = GsonBuilder().registerTypeAdapter(Data::class.java, DataDeserializer()).create()

Consuming Polymorphic Jsons with Retrofit and Kotlin

My API sends me a polyphonic Json in with the variable addon_item can be either a String or an Array, I have spend days trying to make a CustomDezerializer for it without any success.
Here is the Json response:
({
"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
}]
}]
}
})
Here is the Custom Dezerializer, which includes BodyConverter that removes two braces that encompassed the Json response:
'''
/**
* 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)
}
}
}
Here is the 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!!
}
}
}
Finally my 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
}
I took a shot at this and came up with 2 possible ideas. I don't think they're the only way to achieve this, but I think I can share my thoughts.
First, I've reduced the problem to actually only parsing the items. So I've removed retrofit from the equation and use the following 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()
(for when the addon_item is an array)
val jsonString = """{
"addon_item": "foo"
}
""".trimIndent()
(for when the addon_item is a string)
First approach
My first approach was to model addon_item as a generic JsonElement:
data class ItemDetails(
#Expose
#SerializedName("addon_item")
val addonItem: JsonElement? = null
)
(I'm using data classes because I find them more helpful, but you don't have too)
The idea here is to let gson deserialize it as a generic json element and you can then inspect it yourself. So if we add some convenience methods to the class:
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
}
So as you can see, we check the addOnItem for what it contains and according to that, we can obtain its contents. Here's an example of how to use it:
fun main() {
val item = Gson().fromJson(jsonString, ItemDetails::class.java)
println(item.isAddOnItemArray())
println(item.isAddOnItemString())
println(item.addOnItemAsString())
}
I think the biggest advantage of this is that it's fairly simple and you don't require custom logic to deserialize. For me, the huge drawback is the type-safety loss.
You can get the add on as an array, but it will be an array of json elements that have to be "manually" deserialized. Hence, my 2nd approach tries to tackle this.
Second approach
The idea here is to use Kotlin's sealed classes and have 2 types of add ons:
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
}
The SubCategory class is just what was inside the list. Here's a simple version of it:
data class SubCategory(
#SerializedName("subcat_id")
val id: String
)
As you can see the AddOnItems is a sealed class that has the only 2 possible types for your use case.
Now we need a 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")
}
}
In a nutshell, this checks if add on is an array and creates the respective class and the same for string.
Here's how you can use it:
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])
}
I think the biggest advantage here is that you get to keep the types. However, you still need to check what it is before calling addOnItemsAs*.
Hope this helps

Gson deserialization for a list in Kotlin

This is what my json looks like
{
"sub": "9",
"auth_time": 1559381757,
"idp": "idsrv",
"role": [
"Employer",
"Employee",
"Student"
],
"iss": "",
"aud": "",
"exp": 1574933757,
"nbf": 1559381757
}
This is the object I want to convert this Json into.
data class Claims (
#SerializedName("nameid") val nameId: String,
#SerializedName("unique_id") val uniqueId: String,
#SerializedName("sub") val sub: String,
#SerializedName("unifiedNumber") val unifiedNumber: String,
#SerializedName("role") var roleList: List<Role>
)
I wrote a custom Deserializer (which works in Java) for the List type
class RoleDeserializer : JsonDeserializer<List<Role>> {
private var roleId = 0
#Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MutableList<Role> {
val resultList = ArrayList<Role>()
if (json.isJsonArray) {
for (e in json.asJsonArray) {
resultList.add(Role(id = roleId++, name = e.asString))
}
} else if (json.isJsonObject) {
resultList.add(Role(id = roleId++, name = json.asString))
} else if (json.isJsonPrimitive) {
if ((json as JsonPrimitive).isString)
resultList.add(Role(id = roleId++, name = json.getAsString()))
} else {
throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}
return resultList
}
}
This is how I register my type adapter
val listType: Type = object : TypeToken<List<Role>>() {}.type
val gson = GsonBuilder().registerTypeAdapter(listType, RoleDeserializer()).create()
val claims = gson.fromJson(stringJson, Claims::class.java)
I still get a parse exception stating that
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 161 path $.role[0]
and my RoleDeserializer is never called. Am I doing something wrong while registering the type adapter?
Try to replace
val listType: Type = object : TypeToken<List<Role>>() {}.type
with
val listType: Type = object : TypeToken<MutableList<Role>>() {}.type
The role is String array in JSON
Use this
#SerializedName("role") var roleList: List<String>
Instead of this
#SerializedName("role") var roleList: List<Role>
Try this
data class Claims (
#SerializedName("nameid") val nameId: String,
#SerializedName("unique_id") val uniqueId: String,
#SerializedName("sub") val sub: String,
#SerializedName("unifiedNumber") val unifiedNumber: String,
#SerializedName("role") var roleList: List<String>
)

Proper way for json parsing for mutable structure with gson

I'm getting response from server based on such structure:
{
"success":true,
"data":{"can be some kind of data, array or error message"}
}
What is the right way to map properly data attribute in such situations?
My attempts was to use Any type and cast after to specified type:
data class GeneralResponseModel(
val success: Boolean,
val data: Any
)
Provider
//
val response = gson.fromJson(it[0].toString(), GeneralResponseModel::class.java)
//
ViewModel
////////
if (res.success) {
isLoading.postValue(false)
///////
} else {
val result = res.data as ResponseError
errorMessage.postValue(ErrorWrapper(ErrorType.REQUEST_ERROR,result.detail,result.title))
isLoading.postValue(false)
}
///////////
And I got
io.reactivex.exceptions.OnErrorNotImplementedException:
com.google.gson.internal.LinkedTreeMap cannot be cast to
com.myapp.model.response.ResponseError
Another attempt was in using empty interface which was implemented by all possible response types. In this situation I got
java.lang.RuntimeException: Unable to invoke no-args constructor for
interface com.myapp.model.response.Response.
Registering an InstanceCreator with Gson for this type may fix this
problem.
I'm not sure about proper way to handle such trivial case. Any links, code examples or help appreciated. Thanks in advance.
Update
Thanks to Niklas I reconsidered gson into such structure:
lateinit var gson: Gson
when (methodName) {
RequestList.LOGIN.methodName -> {
gson =
GsonBuilder().registerTypeAdapter(
GeneralResponseModel::class.java,
object : JsonDeserializer<GeneralResponseModel> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): GeneralResponseModel {
val gsonInner = Gson()
val jsonObject: JsonObject = json!!.asJsonObject
lateinit var generalResponseModel: GeneralResponseModel
generalResponseModel = if (!jsonObject.get("success").asBoolean) {
GeneralResponseModel(
false,
gsonInner.fromJson(jsonObject.get("data"), ResponseError::class.java)
)
} else {
GeneralResponseModel(
true,
gsonInner.fromJson(jsonObject.get("data"), DriverData::class.java)
)
}
return generalResponseModel
}
}).create()
}
RequestList.GET_JOBS.methodName -> {
gson = GsonBuilder().registerTypeAdapter(
GeneralResponseModel::class.java,
object : JsonDeserializer<GeneralResponseModel> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): GeneralResponseModel {
val gsonInner = Gson()
val jsonObject: JsonObject = json!!.asJsonObject
lateinit var generalResponseModel: GeneralResponseModel
generalResponseModel = if (!jsonObject.get("success").asBoolean) {
GeneralResponseModel(
false,
gsonInner.fromJson(jsonObject.get("data"), ResponseError::class.java)
)
} else {
GeneralResponseModel(
true,
gsonInner.fromJson(jsonObject.get("data"), Array<JobResponse>::class.java)
)
}
return generalResponseModel
}
}).create()
}
else -> gson = Gson()
}
Since your data is very generic, it can't really be parsed in a type-safe way. Gson can't infer types based on pure text (in your specific case there is nothing telling Gson that your data is a ResponseError).
I would consider a general wrapper class like yours, and then using a GSON TypeAdapter to parse the response to your general wrapper.
You have to instantiate GSON with a Builder to define a custom TypeAdapter.
registerTypeAdapter(Type type, Object typeAdapter)
Configures Gson for custom serialization or deserialization.
Your wrapper:
public class Response<T> {
T data;
String message;
public Response(T data, String message) {
this.data = data;
this.message = message;
}
boolean hasData() {
return data != null;
}
T getData() {
return data;
}
String getMessage() {
return message;
}
}
Initialization:
GsonBuilder builder = new GsonBuilder();
b.registerTypeAdapter(Response.class, new JsonDeserializer<Response>() {
#Override
public Response deserialize(JsonElement arg0, Type arg1,
JsonDeserializationContext arg2) throws JsonParseException {
// ... create Response object here
return response;
}

How to get GSON & Retrofit to serialize and map an array from a JSON response into a String?

I am making an API request which returns some array values. I need to serialize these array values so that I can assign them to their corresponding class attributes (which are String types).
Now I know how to use GSON to serialize and deserialize lists, but with Retrofit the mapping is done automatically. This means that if my attribute is of type String, the API call returns the error "Expected a String but received an Array instead". How do I get around this so that I can receive them as arrays without failure, and them store them as strings subsequently?
My API Response:
{
"utterances": [{
"langs": ["eng", "afr", "xho", "zul"],
"utts": [
"Have you been here before?",
"Was u al hier gewees?",
"Ingaba wakhe weza apha ngaphambili?",
"Ingabe uke weza lapha ngaphambilini?"
],
"responses": [
["Yes", "No"],
["Ja", "Nee"],
["Ewe", "Hayi"],
["Yebo", "Cha"]
]
},
{
"langs": ["eng", "afr", "xho", "zul"],
"utts": [
"How are you?",
"Hoe gaan dit met jou?",
"unjani?",
"unjani?"
],
"responses": [
["Good", "Bad"],
["Goed", "sleg"],
["ezilungileyo", "ezimbi"],
["kuhle", "kubi"]
]
}
]
}
My UtteranceResponse class:
class UtteranceResponse {
#SerializedName("status")
var status: String? = null
#SerializedName("count")
var count: Int = 0
#SerializedName("utterances")
var utterances: ArrayList<Utterance>? = null
}
My Utterance class:
class Utterance: SugarRecord {
#SerializedName ("langs")
var langs: String? = null
#SerializedName ("utts")
var utterances_text: String? = null
var utterances_tts: String? = null
#SerializedName ("responses")
var responses_text: String? = null
constructor(){
}
}
And finally the calling function:
fun getUtterancesFromWebservice (){
val apiService = ApiInterface.create()
val call = apiService.getUtteranceDetails()
call.enqueue(object: Callback<UtteranceResponse> {
override fun onResponse(call: Call<UtteranceResponse>, response: retrofit2.Response<UtteranceResponse>?) {
if (response != null) {
if (response.body()?.utterances != null){
var list: List<Utterance> = response.body()?.utterances!!
val utterances: Utterance = list[0]
//storeUtterancesFromList(list)
} else {
Log.d ("Response:", response.body().toString())
}
}else{
Log.d ("responseResult", "NULL")
}
}
override fun onFailure(call: Call<UtteranceResponse>, t: Throwable) {
Log.e("SHIT", t.toString())
}
})
}
UPDATE
My API Interface as well:
#GET("bins/1ahazo")
abstract fun getUtteranceDetails():Call<UtteranceResponse>
companion object Factory {
const val BASE_URL = "https://api.myjson.com/"
fun create(): ApiInterface {
val gson = GsonBuilder().setPrettyPrinting().create()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiInterface::class.java)
}
}
You are returning single object not list. Change Call<UtteranceResponse> in ApiInterface to
Call<List<Utterance>>
and for converting list to string list to string and string to list
class Utterance: SugarRecord {
#SerializedName ("langs")
var langs: List<String?>? = null
#SerializedName ("utts")
var utterances_text: String? = null
var utterances_tts: List<String?>? = null
#SerializedName ("responses")
var responses_tex:List<List<String?>?>? = null;
constructor(){
}
}

Categories

Resources