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
Related
I am currently implementing an API client with Ktor. The API I am requesting does not return a consistent JSON format.
for Example:
sometimes the JSON looks like this:
{
"description": {
"lang": "en",
"value": "an English description..."
},
...
}
and sometimes like this:
{
"description": [
{
"lang": "en",
"value": "an English description..."
},
{
"lang": "fr",
"value": "a French description..."
}
],
...
}
Now my Question:
How can I implement a Custom Kotlinx Deserializer to Decode an Object of T or a List<T> to a List<T>
My classes look like this:
#Serializable
class ResourceResponse(
#SerialName("description")
val descriptions: List<Description>
) {
#Serializable
data class Description(
#SerialName("value")
val value: String,
#SerialName("lang")
val language: String,
)
}
I want that a Json with only one Description-Object will be deserialized to a List with one Object and not specifically for the description, but in general for classes.
I've found nothing really helpful in the Web.
One solution is to first deserialize it to JsonElement, introspect and then decide how to deserialize it further into ResourceResponse:
fun decode(s: String): ResourceResponse {
val json = Json.parseToJsonElement(s).jsonObject
return when (val desc = json["description"]) {
is JsonArray -> Json.decodeFromJsonElement(json)
is JsonObject -> {
val json2 = json.toMutableMap()
json2["description"] = JsonArray(listOf(desc))
Json.decodeFromJsonElement(JsonObject(json2))
}
else -> throw IllegalArgumentException("Invalid value for \"description\": $desc")
}
}
This solution is definitely not ideal. It may be potentially less performant as we need to deserialize the whole tree into the tree of JsonElement objects only to transform it to the final types (although, maybe the library does this internally anyway). It works only for json and it is tricky to use this solution if ResourceResponse is somewhere deep into the data structure.
You can use a JsonContentPolymorphicSerializer to choose a deserializer based on the form of the JSON.
This one should work:
#Suppress("UNCHECKED_CAST")
class DescriptionsSerializer : JsonContentPolymorphicSerializer<List<ResourceResponse.Description>>(
List::class as KClass<List<ResourceResponse.Description>>
) {
// Here we check the form of the JSON we are decoding, and choose
// the serializer accordingly
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out List<ResourceResponse.Description>> {
return if (element is JsonArray)
ListSerializer(ResourceResponse.Description.serializer())
else
SingleDescriptionAsList()
}
class SingleDescriptionAsList : KSerializer<List<ResourceResponse.Description>> {
override val descriptor: SerialDescriptor
get() = ResourceResponse.Description.serializer().descriptor
override fun deserialize(decoder: Decoder): List<ResourceResponse.Description> {
return listOf(ResourceResponse.Description.serializer().deserialize(decoder))
}
override fun serialize(encoder: Encoder, value: List<ResourceResponse.Description>) {
throw Exception("Not in use")
}
}
}
You must also amend your original class to tell it to use this serializer:
#Serializable
class ResourceResponse(
#SerialName("description")
#Serializable(with = DescriptionsSerializer::class) val descriptions: List<Description>
) {
#Serializable
data class Description(
#SerialName("value")
val value: String,
#SerialName("lang")
val language: String,
)
}
Then you will be able to decode JSON objects with the single key "descriptions" using the ResourceResponse serializer.
For avoidance of doubt, if there are other keys in the JSON (it's not entirely clear from the question) then those should also be written into ResourceResponse definition.
After my research, I have now come up with a solution. For this you need a wrapper class. (here GenericResponse). I hope I can help others who have the same problem.
This is the Wrapper-Class
#Serializable(with = ListOrObjectSerializer::class)
class GenericResponse<T>(
val data: List<T> = emptyList()
) {
private var _isNothing : Boolean = false
val isNothing: Boolean
get() {
return this._isNothing
}
companion object {
fun <T> nothing(): GenericResponse<T> {
val o = GenericResponse(emptyList<T>())
o._isNothing = true
return o
}
}
}
And the Serializer looks like:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
class ListOrObjectSerializer<T : Any>(private val tSerializer: KSerializer<T>): KSerializer<GenericResponse<T>> {
override val descriptor: SerialDescriptor
get() = tSerializer.descriptor
override fun deserialize(decoder: Decoder): GenericResponse<T> {
val input = decoder as JsonDecoder
val jsonObj = input.decodeJsonElement()
return when(jsonObj) {
is JsonObject -> GenericResponse(listOf(Json.decodeFromJsonElement(tSerializer, jsonObj)))
is JsonArray -> GenericResponse(Json.decodeFromJsonElement(ListSerializer(tSerializer), jsonObj))
else -> return GenericResponse.nothing()
}
}
override fun serialize(encoder: Encoder, value: GenericResponse<T>) {
throw IllegalAccessError("serialize not supported")
}
}
My Data-Class look now like:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
class ResourceResponse(
#SerialName("description")
val descriptions: GenericResponse<Description>? = null,
) {
#Serializable
data class Description(
#SerialName("value")
val value: String? = null,
#SerialName("lang")
val language: String? = null,
)
}
data class ResourceResponse(
#SerializedName("description") val descriptions: List<Description>,
)
data class Description(
#SerializedName("value") val value: String,
#SerializedName("lang") val language: String,
)
it should be like that
Previously, I asked this question: Implementing generic method in interface that uses implementors class which allowed for an object to be converted to a JSON string.
But, now I would like to reverse the process. Ideally this would look like:
interface Domain {
constructor(json: String) {
/*...*/
}
}
#Serializable
class User(val a: Int, val b: Int): Domain {}
val user = User("{a: 3, b: 4}")
But, I'm unable to figure out how to construct an object directly from a JSON string.
A next best option would be create a static generator method:
interface Domain {
companion object {
inline fun <reified T> fromJSON(json: String): T {
return Json.decodeFromString(json)
}
}
}
val user = User.fromJSON("{a: 3, b: 4}")
But, this doesn't work at all because User does not inherit Domain's companion object. The 3rd best option:
val user = Domain.fromJSON<User>("{a: 3, b: 4}")
This does work from the Android side, however since fromJSON is declared inline and reified it is not exposed to iOS at all from the KMM.
Which brings me to my current solution:
#Serializable
class User(val a: Int, val b: Int): Domain {
companion object {
fun fromJSON(json: String): User { return Json.decodeFromString(json) }
}
}
val user = User.fromJSON("{a: 3, b: 4}")
This works, however it requires the above boilerplate code to be added to each and every 'Domain' object.
Is there anyway to improve on my current solution? (Of course, the higher up the chain the better.)
I think you have Object then you should need to convert it as :--
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.codewithfun.kotlin.jsonparser.models.Tutorial
fun main(args: Array<String>) {
val gson = Gson()
val gsonPretty = GsonBuilder().setPrettyPrinting().create()
val tutsList: List<Tutorial> = listOf(
Tutorial("Tut #1", "bezkoder", listOf("cat1", "cat2")),
Tutorial("Tut #2", "zkoder", listOf("cat3", "cat4"))
);
val jsonTutsList: String = gson.toJson(tutsList)
println(jsonTutsList)
val jsonTutsListPretty: String = gsonPretty.toJson(tutsList)
println(jsonTutsListPretty)
}
Then the output be like :--
[{"title":"Tut #1","author":"bezkoder","categories":["cat1","cat2"]},{"title":"Tut #2","author":"zkoder","categories":["cat3","cat4"]}]
[
{
"title": "Tut #1",
"author": "bezkoder",
"categories": [
"cat1",
"cat2"
]
},
{
"title": "Tut #2",
"author": "zkoder",
"categories": [
"cat3",
"cat4"
]
}
]
My objectmapper.readValue() function throws an error that says "Cannot construct instance of MyErrorMessage (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value."
What's wrong with my MyErrorMessage class that makes objectmapper fail?
This is the JSON I'm trying to parse.
{
"meta": {
"id": "43225a4853b5497a",
"time": "2020-06-03T13:36:03.391814Z"
},
"datadetail": {
"aKey": "hweriu-erw",
"aTypes": [
{
"bKey": "ewrf-7e9f",
"cKey": "12ddf3",
"status": "ERROR",
"errorMessage": {
"message": "Not found"
}
}
],
"status": "ONE",
"errorMessage": "ERROR with aKey"
}
}
This is the function and the classes.
private fun ParseResponse(responseMessage: String): MyResponse{
try {
val objectMapper = ObjectMapper()
return objectmapper.readValue(message, MyResponse::class.java)
}catch (e: JsonProcessingException) {
throw IllegalArgumentException("json was invalid $responseMessage", e)
}
}
data class MyResponse(
val meta: Metainfo,
val datadetail: DataResponse
)
data class MetaInfo(
val id: String,
val time: Instant
)
data class DataResponse(
val aKey: MyKey,
val aTypes: List<TypesResponse>,
val aNumber: String? = null,
val status: StatusType,
val errorMessage: MyErrorMessage = null
)
enum class StatusType{
OK,
ERROR
}
data class TypesResponse(
val bKey: MyBKey,
val cKey: MyCKey,
val status: StatusType,
val errMessage: MyErrorMessage? = null
)
data class MyErrorMessage(
#JsonProperty("message")
val message: String,
#JsonProperty("context")
val context: MyContext?,
){
constructor(message: String) : this(
message = message,
context = null
)
enum class MyContext{
ONE, TWO, THREE
}
}
Your JSON does not follow your class structure.
You have "errorMessage": "ERROR with aKey", but your errorMessage in DataResponse is in fact a MyErrorMessage which is an object, not a simple String.
It would need to be:
"errorMessage": {
"message": "ERROR with aKey"
}
If that is not an option, then you need a custom Jackson JSON Deserializer for your MyErrorMessage that can handle that. You can check an example here.
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')
I'm successfully receiving a JSON Object as request and passing it on to my parser. Code runs through until I call fromJson and then gets stuck. What am I doing wrong?
Here's the corresponding class:
class User(#SerializedName("mac") private val phoneMac: String) : Comparable<User> {
#SerializedName("values")
private val measurements: MutableSet<Measurement> = mutableSetOf()
fun getPhoneMac(): String = phoneMac
fun getMeasurements(): Set<Measurement> = measurements
//etc.
}
Which refers to this class:
class Measurement (#SerializedName("mac") val deviceMac: String, val timestamp: String, val value: Double, val valueType: ValueType) : Comparable<Measurement>{
fun getDeviceMac(): String = deviceMac
fun getTimestamp(): String = timestamp
fun getValue(): Double = value
fun getValueType(): ValueType = valueType
//etc.
}
And here is how I try to parse it:
fun fromJson(json: String): User {
val builder = GsonBuilder()
builder.setPrettyPrinting()
return builder.create().fromJson(json, User::class.java)
}
Had the fromJson-function spreaded out to make sure where it gets stuck: create() still works, fromJson() doesn't
Also, I know that the JSON file is correct and doesn't contain missing values or nulls.
For verification:
{
"mac": "00-80-41-ae-fd-b1",
"values":
[
{
"mac": "ab-cd-ef-98-76-13",
"timestamp": "2012-04-23T18:25:43",
"value": 68,
"valuetype": "HR"
},
{
"mac": "ab-cd-ef-98-76-13",
"timestamp": "2012-04-23T18:35:43",
"value": 65,
"valuetype": "HR"
}
]
}
Edit: for clarification as to what I mean with my code getting stuck
For debugging purposes, I changed my fromJson function to look like this:
fun fromJson(json: String): User {
val builder = GsonBuilder()
builder.setPrettyPrinting()
println("json received")
val gson = builder.create()
println("GSON created")
val user = gson.fromJson(json, User::class.java)
println("user created")
return user
}
My Console reads
json received
GSON created
Meaning "user created" is not printed, therefore the gson.fromJson-call never returns
Not sure what you mean by getting stuck, but this seems to work:
import com.google.gson.*
import com.google.gson.annotations.*
data class User(#SerializedName("mac") val phoneMac: String, #SerializedName("values") val measurements: MutableSet<Measurement>)
enum class ValueType{
HR
}
data class Measurement (#SerializedName("mac") val deviceMac: String, val timestamp: String, val value: Double, val valuetype: ValueType)
fun fromJson(json: String): User {
val builder = GsonBuilder()
builder.setPrettyPrinting()
return builder.create().fromJson(json, User::class.java)
}
fun main() {
println(fromJson("""
{
"mac": "00-80-41-ae-fd-b1",
"values":
[
{
"mac": "ab-cd-ef-98-76-13",
"timestamp": "2012-04-23T18:25:43",
"value": 68,
"valuetype": "HR"
},
{
"mac": "ab-cd-ef-98-76-13",
"timestamp": "2012-04-23T18:35:43",
"value": 65,
"valuetype": "HR"
}
]
}
""".trimIndent()))
}
It appears the fix was to get rid of getter functions in Measurement-class or setting the fields in Measurement-class private.
I copy/pasted barsju's code, and it worked. So step by step tried to swap in pieces of my code. After swapping in my Measurments-class, I got the following exception:
java.lang.ClassFormatError: Duplicate method name "getDeviceMac" with signature "()Ljava.lang.String;" in class file jsonStuff/Measurement
Did some experimentation and research only to find that if the fields of Measurement are not private but still have getter functions explicitly declared, they're recognized as duplicate functions, basically killing the application
I would suggest, if you can, use data classes, and remove the getters and the Comparable as well.
Also that's one of the differences of the example answers some of the users gave you here.