I have a json like this. I need to convert it to data class
{
"0": {
"id": "111",
"type": "1",
"items": [
{
"name": "Jack",
"value": "26",
"age": "0.0"
},
{
"name": "Lisa",
"value": "18",
"age": "1.0"
}
]
},
"1": {
"id": "222",
"type": "2",
"items": [
{
"name": "Brown",
"value": "23",
"age": "30.0"
},
{
"name": "Andy",
"value": "18",
"age": "23.0"
}
]
},
"className": "A01"
}
I define the following data class
data class Orders (
val className: String?,
val classes: Map<String, EachClass>
)
data class EachClass (
val id: String,
val type: String,
val items: List<Person>
)
data class Person (
val name: String,
val value: String,
val age: String
)
And the result always show
className=> A01, classes=> null
I searched the stackoverflow and they said using TypeToken. But I have a field called "className" which cannot be convert with EachClass object
val type = object : TypeToken<EachClass>() {}.type
val obj = Gson().fromJson(data, EachClass::class.java)
and I found TypeToken with HashMap<String, Object> is working but its ugly and I need to convert to data class myself.
I'm appreciate if someone can tell me the correct way to convert the json. Thanks!
Gson does not provide built-in functionality for this specific situation so you need to do some manual conversion, but luckily for your use case it is not that much work. The following approach should work:
Parse the JSON as Gson's JsonObject
Remove the className member and store it for later
Parse the JsonObject as Map<String, EachClass>
Construct an Orders instance from the results from step 2 and 3
The complete solution could look like this:
object OrdersDeserializer: JsonDeserializer<Orders> {
private val classesType = object: TypeToken<Map<String, EachClass>>() {}.type
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Orders {
val jsonObject = json.asJsonObject
val className = jsonObject.remove("className").asJsonPrimitive.asString
val classes: Map<String, EachClass> = context.deserialize(jsonObject, classesType)
return Orders(className, classes)
}
}
You would then register it like this:
val gson = GsonBuilder()
.registerTypeAdapter(Orders::class.java, OrdersDeserializer)
.create()
Alternatively you could also convert it to a regular class and use Gson's #JsonAdapter annotation on the Orders class to avoid having to register the deserializer manually.
Note: Normally is recommended to prefer TypeAdapter over JsonSerializer / JsonDeserializer to allow streaming the data for better performance. However, since you need to work on a JsonObject here anyway (therefore non-streaming) using TypeAdapter does not provide an advantage here and might only complicate the implementation a bit.
Related
I have this json response from the api, and the response can't be change
data class Weather (
val category: String,
val id: String,
val meta: Meta
)
data class Meta (
val id: String,
val name: String,
val details: String
)
Json respose
{
"weather" : {
"category": "articles",
"id": "1",
"meta": {
"id": "1",
"name": "The shortest article. Ever.",
"details": "see"
},
"weather" : {
"category": "articles",
"id": "2",
"meta": []
}
If meta is empty, it come with an array but if not empty, it come with object.
Retrofit throws
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY
the api can't be modify so this has to be fix on client end. How can I solve this
You can not make this possible
meta object must be an object when have value and null when do not have any value, or an array with value when exist and empty when not exist.
meta can not be an array and object in the same time.
this is very bad mistake from who created this response body.
You can use Any type for meta. and put check at your code level like this.
data class Weather (
val category: String,
val id: String,
val meta: Any
)
if(meta is Meta)
parse it to your Meta object
else
parse it to list
I want to parse JSON like below:
{
"result": 0,
"list": [
{
"id": 58,
"type": "58",
"name": "fooGroup",
"foos": {
"id": "1",
"name": "33",
}
]
}
If I define models like this:
data class Response(val result: Int,
#SerializedName("list") val fooGroup: List<FooGroupResponse>)
data class FooGroupResponse(val id: Int, val type: String, val name: String,
#SerializedName("foos") val fooGroup: List<Foo>?)
data class Foo(val id: Int, val name: String)
then everything works fine.
Right now I want to take these out as a model:
"id": 58,
"type": "58",
"name": "fooGroup",
That is add one more model FooGroup like below:
data class Response(val result: Int,
#SerializedName("list") val fooGroup: List<FooGroupResponse>)
data class FooGroupResponse(val fooGroup: FoolGroup,
#SerializedName("foos") val fooGroup: List<Foo>?)
data class Foo(val id: Int, val name: String)
data class FooGroup(val id: Int, val type: String, val name: String)
But there's no #SerializedName can be set for FooGroup, is it possible?
Thanks.
I think you will have to restructure your JSON. The parser is going to make a faithful representation of the JSON string as a Java object. Your first example is the faithful representation. What you want to do is not faithful to the JSON received.
{
"result": 0,
"list": [
{
"fooGroup": {
"id": 58,
"type": "58",
"name": "fooGroup"
},
"foos": [
{
"id": "1",
"name": "33"
}
]
}
]
}
BUT you can do it manually where you parse the object yourself. Here is a how to with GSON but it should be easily convertible if you prefer another lib.
https://www.woolha.com/tutorials/retrofit-2-define-custom-gson-converter-factory
Simply pull "id", "type", "name" from the JSON as its deserializing and make a composite data class.
data class FooGroup(val id: Int, val type: String, val name: String)
//You deserialize you JSON into a List of Foos
data class Foos(val foo: Foo, val fooGroup : FooGroup)
I have this response, i have problem when i want to convert to pojo.
"equity": {
"0": {
"name": [
"Abc"
],
"code": [
"3410"
],
"ending_balance": [
301834470
]
},
"1": {
"name": [
"Xyz"
],
"code": [
"2180"
],
"ending_balance": [
0
]
},
"2": {
"name": [
"Pqr"
],
"code": [
"9220"
],
"ending_balance": [
0
]
},
"total_equity": 301834470
}
}
I'm confused about giving the right data type, because there are arrays("0","1","2") that contain objects and "total_equity" that contain number.
I've tried to give the map data type, but it will be error for "total_equity"
var equity: Map<String, EquityDto?>
If you know the solution for this problem please help me. Thank you
You can use
var equity: Map<String, Any>
While accessing the Any Data type u can compare the varrriable type(instance of) and use the value as following
if (valueofMap is Int){
//here is integer value
}
if (valueofMap is yourDataClass){
//your custom class
}
There can be a solution to this, but it will be a lengthy one.
One solution can be to transform the response to Map<String, Any> then you will have to check the type every time you have to use it and it can really annoying when you are using it in multiple classes.
Another solution can be to create a custom Custom Type Adapter which you can pass to the Retrofit Instance in the addConverterFactory method.
To create a custom adapter, you just have to follow the following steps:
Create a model in which you want to store the data. In your case it can be :
data class ApiResponse(
val data: Map<String, EquityDto?>,
val totalEquity:Int
)
Create the Adapter:
class StudentAdapter : TypeAdapter<ApiResponse?>() {
#Throws(IOException::class)
fun read(reader: JsonReader): ApiResponse {
val student:ApiResponse? = null
reader.beginObject()
var fieldname: String? = null
while (reader.hasNext()) {
var token = reader.peek()
if (token == JsonToken.NAME) {
//get the current token
fieldname = reader.nextName()
}
if ("total_equity" == fieldname) {
token = reader.peek()
student?.totalEquity = reader.nextInt()
}else {
token = reader.peek()
student?.data?.set(fieldname.toString(), reader.nextString())
}
}
reader.endObject()
return student
}
#Throws(IOException::class)
fun write(writer: JsonWriter, student: ApiResponse) {
writer.beginObject()
writer.name("data")
writer.value(student.data.toString())
writer.name("totalEquity")
writer.value(student.totalEquity)
writer.endObject()
}
}
If you know a better way to create a type adapter then you can surely use that.
Because Gson uses reflection to build objects during deserialization via a no-arguments constructor, I am writing a TypeAdapter that will instead use the appropriate constructor for my object. I need this to happen because the object I am deserializing is a subclass of another and needs to call the super constructor appropriately.
I'm trying to deserialize a JSON array that looks something like this (this is example data, not the actual structure of my object):
[
{
"id": "...",
"name": "...",
"description": "...",
"others": [
{
"key1": "val",
"key2": "val"
},
...
]
},
{
"id": "...",
"name": "...",
"description": "...",
"others": [
{
"key1": "val",
"key2": "val"
},
...
]
},
...
]
But I want to use a TypeAdapter during the deserialization of the objects that comprise this array. To be clear, this is how I am initiating the Gson deserialization:
new GsonBuilder()
.registerTypeAdapter(new TypeToken<MyObject>() {}.getType(), new MyTypeAdapter())
.create()
.fromJson(inputJson, MyObject[].class);
And this is MyTypeAdapter:
class MyTypeAdapter : TypeAdapter<MyObject> {
private val type = object : TypeToken<MyObject>() {}.type
override fun write(out: JsonWriter, myObject: MyObject) {
Gson().toJson(myObject, type, out))
}
override fun read(in: JsonReader): MyObject {
Log.d("MyTypeAdapter", "Deserialization via custom TypeAdapter")
val objByReflection = new Gson().fromJson(in, type)
return MyObject(objByReflection.id, objByReflection.name, objByReflection.description, objByReflection.others)
}
}
However, I find that the TypeAdapter is not used for the deserialization; setting a breakpoint doesn't find it being hit, and I get no log output.
Is this happening because I am actually deserializing an array of MyObject? What would be a good way to remedy this?
Thanks!
In the response from the server, the data is structured as named objects. I'm trying to figure out how I would convert it to an array with the details in specific fields.
Example response from the server.
{
"Value_1": { "Foo": "True", "Bar": "False"},
"Value_2": { "Foo": "False", "Bar": "False"},
"Value_3": { "Foo": "False", "Bar": "True"}
}
Example of preferred converted result from the server.
{[
{"Name": "Value_1",
"Details": [{"Name": "Foo", "Value": "True"},
{"Name": "Bar", "Value": "False"}]},
{"Name": "Value_2",
"Details": [{"Name": "Foo", "Value": "False"},
{"Name": "Bar", "Value": "False"}]},
{"Name": "Value_3",
"Details": [{"Name": "Foo", "Value": "False"},
{"Name": "Bar", "Value": "True"}]}
]}
How do tell gson to convert from the response to the preferred structure?
I was able to accomplish this in a fairly clean method using a custom JsonDeserializer. The main thing is JsonObject.entrySet(), which gives you the key/value pairs of the JsonObject so you can iterate over them.
First, when building your Retrofit client, add your custom JsonDeserializer.
gsonBuilder.registerTypeAdapter(MyModel::class.java, MyModelDeserializer())
And then implement it like this.
class MyModelDeserializer : JsonDeserializer<MyModel> {
override fun deserialize(json : JsonElement, typeOfT : Type?, context : JsonDeserializationContext?) : MyModel {
val jsonObject = json.asJsonObject
// Create a new ArrayList to store the values.
val list = ArrayList<MyModel>()
// Using entrySet, get each key/value pair. "Value_1: {...}"
for (entry in jsonObject.entrySet()) {
val valueName = entry.key // The key, which would be the name "Value_1"
// Creating a new ArrayList to contain all the data.
val vals = ArrayList<Pair<String, Boolean>>()
val values = entry.value.asJsonObject
// Using entrySet again for each value of the Object.
for (entry in values.entrySet()) {
vals.add(entry.key to entry.value.asBoolean)
}
// Create a new MyModel, with the correct name and values.
list.add(MyModel(valueName, vals))
}
return MyModel(list)
}
}