I would like to deserialize the following JSON:
{
"participants": {
"0": {
"layout": "layout1"
}
},
"layouts": {
"layout1": {
"width": 100,
"height": 100
}
}
}
Into the following structure:
#Serializable
data class Layout(val width: Int, val height: Int)
#Serializable
data class Participant(val index: Int, val layout: Layout)
#Serializable
data class ViewData(val participants: MutableMap<Int, Participant>, val layouts: MutableMap<Int, Layout>)
What I'm particularly struggling with is how to create the correct relationship between participant's layout using the key "layout1" in the "layouts" hash.
Thanks!
You have to create the classes that matches the json string
data class Layout(
val width: Int,
val height: Int
)
data class Participant(
val layout: String
)
data class ViewData(
val participants: Map<String, Participant>,
val layouts: Map<String, Layout>
)
It is not possible (by default) to deserialize map<String, Any> to map<Int, Any> and it is not possible to add magical val index: Int
then create a function that will help you get layout by participant name
data class ViewData(
val participants: Map<String, Participant>,
val layouts: Map<String, Layout>
) {
fun getLayoutForParticipant(participantId: String): Layout? {
val layoutId = participants[participantId]?.layout
return layoutId?.let { layouts[it] }
}
}
val json: String = """
{
"participants": {
"0": {
"layout": "layout1"
}
},
"layouts": {
"layout1": {
"width": 100,
"height": 100
}
}
}
"""
val deserialized: ViewData = objectMapper.readValue(json, ViewData::class.java)
println(deserialized)
println(deserialized.getLayoutForParticipant("0"))
result:
ViewData(participants={0=Participant(layout=layout1)}, layouts={layout1=Layout(width=100, height=100)})
Layout(width=100, height=100)
Related
I have JSON object like this
{
"codemap":{
"codeOfItem1":"titleOfItem1",
"codeOfItem2":"titleOfItem2",
"codeOfItem3":"titleOfItem3",
"codeOfItem4":"titleOfItem4"
},
"items":{
"titleOfItem1":{
"attribute1":"value1",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item1",
"subattr2":"value1_of_subattr2_for_item1"
}
},
"titleOfItem2":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item2",
"subattr2":"value1_of_subattr2_for_item2"
}
},
"titleOfItem3":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item3",
"subattr2":"value1_of_subattr2_for_item3"
}
},
"titleOfItem4":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item4",
"subattr2":"value1_of_subattr2_for_item4"
}
}
}
}
How to parse it using GSON in Kotlin ?
(Problem is that strings like titleOfItemXXX is both values in codemap map and key names in items map
I don't really like idea to go fully manual way like in How to parse this Json with no object name
Update:
I don't want to get scheme like this (this is from Kotlin-to-JSON Android Studio Plugin)
import com.google.gson.annotations.SerializedName
data class x1(
#SerializedName("codemap")
val codemap: Codemap,
#SerializedName("items")
val items: Items
) {
data class Codemap(
#SerializedName("codeOfItem1")
val codeOfItem1: String, // titleOfItem1
#SerializedName("codeOfItem2")
val codeOfItem2: String, // titleOfItem2
#SerializedName("codeOfItem3")
val codeOfItem3: String, // titleOfItem3
#SerializedName("codeOfItem4")
val codeOfItem4: String // titleOfItem4
)
data class Items(
#SerializedName("titleOfItem1")
val titleOfItem1: TitleOfItem1,
#SerializedName("titleOfItem2")
val titleOfItem2: TitleOfItem2,
#SerializedName("titleOfItem3")
val titleOfItem3: TitleOfItem3,
#SerializedName("titleOfItem4")
val titleOfItem4: TitleOfItem4
) {
data class TitleOfItem1(
#SerializedName("attribute1")
val attribute1: String, // value1
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item1
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item1
)
}
data class TitleOfItem2(
#SerializedName("attribute1")
val attribute1: String, // value2
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item2
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item2
)
}
data class TitleOfItem3(
#SerializedName("attribute1")
val attribute1: String, // value2
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item3
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item3
)
}
data class TitleOfItem4(
#SerializedName("attribute1")
val attribute1: String, // value2
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item4
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item4
)
}
}
}
because I don't really known how much items I will have and which names they will use in production.
I think you want dynamic-named property in the schema. In this case, declare it as Map:
data class Schema(val codemap: Map<String, String>, val items: Map<String, Item>) {
data class Item(val attribute1: String, val atttribute2: Attr2) {
data class Attr2(val subattr1: String, val subattr2: String)
}
}
fun gsonDemo() {
val json = """
{
"codemap":{
"codeOfItem1":"titleOfItem1",
"codeOfItem2":"titleOfItem2",
"codeOfItem3":"titleOfItem3",
"codeOfItem4":"titleOfItem4"
},
"items":{
"titleOfItem1":{
"attribute1":"value1",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item1",
"subattr2":"value1_of_subattr2_for_item1"
}
},
"titleOfItem2":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item2",
"subattr2":"value1_of_subattr2_for_item2"
}
},
"titleOfItem3":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item3",
"subattr2":"value1_of_subattr2_for_item3"
}
},
"titleOfItem4":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item4",
"subattr2":"value1_of_subattr2_for_item4"
}
}
}
}
""".trimIndent()
val obj = Gson().fromJson(json, Schema::class.java)
println(obj.items[obj.codemap["codeOfItem3"]]?.atttribute2?.subattr1) // print value1_of_subattr1_for_item3
}
Note that Gson won't fail if some properties are missing, so all the values of properties may be null, although they are declared as non-nullable
Looks like I forget about simple ways :(
Working answer:
data class TopLevel (
#SerializedName("codemap")
val codemap: Map<String, String>,
#SerializedName("items")
val items: Map<String, Item>
)
data class Item (
#SerializedName("attribute1")
val attribute1: Attribute1,
#SerializedName("attribute2")
val attribute2: Attribute2
)
data class Attribute2 (
#SerializedName("subattr1")
val subattr1: String,
#SerializedName("subattr2")
val subattr1: String
)
enum class Attribute1 {
#SerializedName("DarkSide")
DarkSide,
#SerializedName("LightSide")
LightSide
}
var gson: Gson = Gson()
val str=... //string with source JSON
val result = gson.fromJson(str, TopLeve::class.java)
Everything appears to work correctly.
{
"results" : [
[ "5bcafde29600021147742e3a",
"ljsdlfj",
"ljdfl",
"url",
"title",
15.2805
],
[ "5bcafde29600021147742e3a",
"ljsdlfj",
"ljdfl",
"url",
"title",
14.2805
],
[ "5bcafde29600021147742e3a",
"ljsdlfj",
"ljdfl",
"url",
"title",
13.2805
]
],
"count": 53,
"total": 53,
"perpage": 10,
"page": 0
}
Although I tried making it using kotlin data class file from JSON - plugin but it resulted
like this -
data class store(
val count: Int,
val page: Int,
val perpage: Int,
val results: List<List<Any>>,
val total: Int
)
See, here it created val results: List<List>, which i don't understand how to use in adapter. Is there any way to solve it?
Hope you understand the question, if you found trouble in getting json data check here
Here's my adapter class, I need url, title and description from results (of Json Data) into pDecp, pTitle and pImg.
class PatentAdapter(val context: Context, val patents: List<List<Any>>) :
RecyclerView.Adapter<PatentAdapter.PatentViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatentViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.patent_item, parent, false)
return PatentViewHolder(view)
}
override fun onBindViewHolder(holder: PatentViewHolder, position: Int) {
val patentData : List<List<Patent>> = patents as List<List<Patent>>
holder.pDecp.text =
holder.pTitle.text =
}
override fun getItemCount(): Int {
return patents.size
}
class PatentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var pTitle = itemView.findViewById<TextView>(R.id.tv_patentTitle)
var pDecp = itemView.findViewById<TextView>(R.id.tv_patentDescription)
var pImg = itemView.findViewById<ImageView>(R.id.iv_patentImg)
}
}
I figured out the solution, it will be great if you can improve it while it works for me ->
Data class would be like this ->
data class Patent(
val count: Int,
val page: Int,
val perpage: Int,
val results: List<List<String>>,
val total: Int
)
And in adapter class we have to take the postion of array or list if we didn't have keys like this ->
class PatentAdapter(val context: Context, val patents: Patent) :
RecyclerView.Adapter<PatentAdapter.PatentViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatentViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.patent_item, parent, false)
return PatentViewHolder(view)
}
override fun onBindViewHolder(holder: PatentViewHolder, position: Int) {
val patentData : List<String> = patents.results[position]
holder.pDecp.text = patentData[3]
holder.pTitle.text = patentData[2]
Glide.with(this.context).load(patentData[10].toString()).into(holder.pImg)
}
override fun getItemCount(): Int {
return patents.results.size
}
class PatentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var pTitle = itemView.findViewById<TextView>(R.id.tv_patentTitle)
var pDecp = itemView.findViewById<TextView>(R.id.tv_patentDescription)
var pImg = itemView.findViewById<ImageView>(R.id.iv_patentImg)
}
}
I have an abstract class "Elem" with a bunch of children (TXT, IMG, EDT ...). They all have contructors.
I need to parse in json an Object contaning a list of children of an abstract class
abstract class Elem(
var content : String,
var condition : String = ""
){
open fun instantiate(){
}
}
class TXT(content: String) : Elem(content) {
override fun instantiate() {
//Some work
}
}
class BTN(content: String, private val additional : String) : Elem(content) {
override fun instantiate() {
//Some work
}
}
...
EDIT :
I tried to used the AbstractElementAdapter, as shown here
Here the new code to parse a JSON
val e = MyObject(/*Some stuff,*/listOf(TXT("Hello"), IMG("world.png"))))
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(EtapElem::class.java, JsonSerializer<EtapElem>{
src, _, context ->
val result = JsonObject()
result.add("type", JsonPrimitive(src.javaClass.simpleName))
result.add("elem", context.serialize(src, src.javaClass))
return#JsonSerializer result
})
val jsonPretty: String = gsonBuilder .setPrettyPrinting().create().toJson(e)
The json looks fine
{
//Some stuff,
"elems": [
{
"type": "TXT",
"elem": {
"content": "Hello?",
"condition": ""
}
},
{
"type": "IMG",
"elem": {
"content": "world.png",
"condition": ""
}
}
]
}
Now the read :
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(EtapElem::class.java, JsonDeserializer<EtapElem>{
json, _, context ->
val jsonObject = json.asJsonObject
val type = jsonObject["type"].asString
val element = jsonObject["elem"]
return#JsonDeserializer try {
context.deserialize(element, Class.forName("com.package.path.elem.$type"))
} catch (cnfe: ClassNotFoundException) {
throw JsonParseException("Unknown element type: $type", cnfe)
}
})
val outtype = object : TypeToken<MyObject>() {}.type
val s : Scenario = gsonBuilder.create().fromJson(jsonFileString, outtype)
I have an exception thrown in read :
java.lang.ClassCastException: class com.package.path.elem.TXT cannot be cast to class java.lang.Void (com.package.path.elem.TXT is in unnamed module of loader 'app'; java.lang.Void is in module java.base of loader 'bootstrap')
Imagine this data sample
"meta_data": [
{
"id": 40097,
"key": "_wcf_frm_created",
"value": ""
},
{
"id": 40098,
"key": "_wcf_custom_degin_checkbox",
"value": ""
},
{
"id": 40099,
"key": "_wcf_frm_data",
"value": {
"1": {
"1": "",
"2": "",
"3": "chk_box"
}
}
},
{
"id": 40119,
"key": "_vendor_select",
"value": "6484"
},
{
"id": 40120,
"key": "_vendor_percentage",
"value": "1"
},
{
"id": 40121,
"key": "_vendor_pro_cat",
"value": "Accessories"
}
]
the Value in Meta_data can have multiple types. In the generator I used shown that the data type should be created like this.
sealed class Value {
class StringMapMapValue(val value: Map<String, Map<String, String>>) : Value()
class StringValue(val value: String) : Value()
}
With Moshi, I understand you have to add #JsonClass(generateAdapter = true) on top of the data class. Thus I have something like this
#JsonClass(generateAdapter = true)
data class MetaDatum (
val id: Long,
val key: String,
val value: Value
)
#JsonClass(generateAdapter = true)
sealed class Value {
class StringMapMapValue(val value: Map<String, Map<String, String>>) : Value()
class StringValue(val value: String) : Value()
}
I would like to note that the full json is much bigger than this. However, this is the only issue I have. I had some Enum issues as well, but those can be replaced with String
The error I received is
error: #JsonClass can't be applied to net......Activity.Value: must not be sealed
public static abstract class Value
Thus my question is, how do i decode the json with multiple enum types.
Ill add this here, In xCode(swift) this was how i was manage to do it.
enum Value: Codable {
case string(String)
case stringMapMap([String: [String: String]])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([String: [String: String]].self) {
self = .stringMapMap(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Value.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Value"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .stringMapMap(let x):
try container.encode(x)
}
}
}
Calling the data
fun retrieveMenu(sku: Int, SSLAuth: String)
{
doAsync {
val client = OkHttpClient().newBuilder()
.build()
val formBody: RequestBody = FormBody.Builder()
.build()
val request: Request = Request.Builder()
.url("https://carteapp.net/..................")
.method("Get", formBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val gist =
gistJsonAdapter.fromJson(response.body!!.source())
println(gist)
}
}
}
private val moshi = Moshi.Builder().build()
private val gistJsonAdapter = moshi.adapter(BarcodeScannerActivity.WcProductCall::class.java)
#JsonClass(generateAdapter = true)
data class WcProductCall (
val id: Long,
...........
val metaData: List<MetaDatum>,
...
)
This is a bit tricky as Moshi won't allow sealed/abstract classes, my idea would be to use the DTO pattern (This is a pattern for using a Data Transfer Object to pass as much data in a single network request as possible)
Create a data class (your DTO) that contains both / all the data
#JsonClass(generateAdapter = true)
data class MetaDatumDTO (
val id: Long,
val key: String,
val value1: Value1DTO?,
val value2: Value2DTO?
)
#JsonClass(generateAdapter = true)
data class Value1DTO(val value: Map<String, Map<String, String>>)
#JsonClass(generateAdapter = true)
data class Value2DTO(val value: String)
And then inside your repository / model, when you retrieve the data, use a mapper function to map the data to the data classes you want to use
data class MetaDatum(
val id: Long,
val key: String,
val value: Value?
)
sealed class Value {
data class Value1(val value: Map<String, Map<String, String>>) : Value()
data class Value2(val value: String) : Value()
}
fun MetaDatumDto.mapFromNetworkRequest(): MetaDatum {
return MetaDatum(
id = id,
key = key,
value = getValueFromDTO()
)
}
fun MetaDatumDto.getValueFromDTO(): Value? {
return (value1 as? Value1) ?: (value2 as? Value2)
}
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
fun main() {
val jsonString: String = """{
"jsonrpc": "2.0",
"id": null,
"result": [
{
"id": 1,
"name": "Lekhnath Rijal"
},
{
"id": 2,
"name": "Administrator"
}
]
}"""
val body1 = Gson().fromJson<RpcResult<List<Partner>>>(jsonString, object: TypeToken<RpcResult<List<Partner>>>(){}.type)
println(body1.result[0].name) // prints Lekhnath Rijal // - As expected
val body2 = fromJson<RpcResult<List<Partner>>>(jsonString)
println(body2.result[0].name) // throws Exception as stated below after this code snippet
}
fun <T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
data class RpcResult<T>(
val jsonrpc: String,
val id: Int?,
val result: T
)
data class Partner(
val id: Int,
val name: String
)
Exception: java.lang.ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class RpcResult
while converting json string to data class object without using function it works as expected but executing same code from helper function does not work and instead throws an exception mentioned above. What am I missing here?
It is due to type erasure in runtime. In Kotlin you can solve this issue by making your function inline with reified type:
Change your function from:
fun <T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
To:
inline fun <reified T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
For further reading check this out: https://kotlinlang.org/docs/reference/inline-functions.html