I am struggling to achieve the following functionality: deserialize dynamic properties (incoming json) into array field/map.
Input:
{
dynamic_field1: "value1",
dynamic_field2: "value2",
...
name: "John",
age: 20
...
}
into Kotlin data class (although being a kotlin data class is not that relevant):
#JsonDeserialize(using = PersonDeserializer::class)
data class Person(
val personName: String,
#JsonProperty(using = SpecificIntDeserializer::class)
val age: Int,
val dymamicProperties: Map<String, String>
)
So essentially use (delegate) provided or default bean deserializer (whenever possible) BUT also get all fields that start with dynamic_ and process them separately.
What I tried so far:
find the ValueInstantiator, iterate through the main constructor (instantiator.withArgsCreator) and for each parameter find the right deserializer; however, I am unable to connect this with right JsonToken
tried ContextualDeserializer and ResolvableDeserializer but got lost.
Any suggestions?
Related
All the API JSON responses would have the following structure:
{
"status": <Integer>
"data": <Object or List of Objects>
"message": <String>
}
the only property that changes is the 'data', which can be any object or list of object.
So is there a way to create a BaseResponse class like
open class BaseResponse<T> (
#SerializedName("status")
val status: Int,
#SerializedName("data")
abstract val `data`: T,
#SerializedName("message")
val message: String
)
and the response classes
data class HelloResponse (
override val `data`: Hello
) : BaseResponse<Hello> {
data class Hello (
#SerializedName("hello")
val hello: String
)
}
data class HellosResponse (
override val `data`: List<Hello>
) : BaseResponse<List<Hello>> {
data class Hello (
#SerializedName("hello")
val hello: String
)
}
What i really want is to only override the data property, so that i don't have to write status and message property for each Response sub data class i write. I dont want to write status and message in my sub class and pass it to base class, cause i'd still write both the properties, so no difference than creating a data class with status and message.
so cannot be like
data class HelloResponse (
val status: Int,
override val `data`: Hello,
val message: String
) : BasicResponse<Hello>(status, `data`, message) {
data class Hello (
#SerializedMessage("hello")
val hello: String
)
}
Edit: Own Answer
Well I realized that the HelloResponse is actually a waste since i'm only using it to access the actual Hello class.
So what i did was to use the Base class directly in Retrofit2 service.
Eg:
fun hello(): Call<BaseResponse<Hello>>
or
fun hellos(): Call<BaseResponse<List<Hello>>>
Well you have to directly specify the type with BaseResponse everywhere you use it. Maybe create typeallias
Or you can create alias
typealias HelloResponse = BaseResponse<Hello>
typealias HellosResponse = BaseResponse<List<Hello>>
To manually deserialize json string with Gson, you need to use TypeToken parameter instead of class type.
val hello = Gson().fromJson<BaseResponse<Hello>>(jsonStr, object: TypeToken<BaseResponse<Hello>>(){}.type)
If you use
val hello = Gson().fromJson<BaseResponse<Hello>>(jsonStr, BaseResponse::class.java)
The data property doesn't convert to Hello instead converts to LinkedHashMap
Note:
Retrofit2's GsonConverterFactory uses TypeToken internally, so no problem.
If you don't want to write status and message properties for data subclasses then you cannot expect subclass to have a constructor with status and message magically.
I strongly suggest you to make BaseResponse abstract and make subclasses like following
abstract class BaseResponse<T> {
#SerializedName("status")
abstract val status: Int
#SerializedName("message")
abstract val message: String
#SerializedName("data")
abstract val `data`: T
}
data class HelloResponse (
override val status: Int,
override val message: String,
override val `data`: Hello,
) : BaseResponse<Hello>() {
data class Hello (
#SerializedMessage("hello")
val hello: String
)
}
You can achieve it in a way you don't need to write override val properties for subclass declarations by sacrificing data classes. However you lose all bounties provided by data class.
abstract class BaseResponse<T> {
#SerializedName("status")
abstract val status: Int
#SerializedName("message")
abstract val message: String
#SerializedName("data")
abstract val `data`: T
}
class HelloResponse: BaseResponse<Hello>() {
data class Hello (
#SerializedMessage("hello")
val hello: String
)
}
Just a kind reminder, you don't need to use #SerializedName annotation if class property name and json property name are same.
I'm new to Quarkus and Kotlin and truth be told, I'm not quite sure yet what goes on behind the scenes and which JSON library is actually responsible for rendering the JSON response from a resource when I set the #Produces(MediaType.APPLICATION_JSON) on my function. But I'm returning an instance of a data class that I created from that method and all of the fields in that data class are rendered in the response. However, I have multiple response classes and I would like to include the name of the class in the JSON response. What I have now is a String field that is simply hard coded to the name of the class but that is ugly as I have to repeat the class name:
data class StuffInitiatedResponse (
val id: String,
val projectId: String
) {
val operation = "StuffInitiatedResponse"
}
data class StuffCompletedResponse (
val id: String,
val projectId: String,
) {
val operation = "StuffCompletedResponse"
}
And in my service class:
#Path("/myservice")
class MyService {
#POST
#Path("{project}/{path:.*}")
#Produces(MediaType.APPLICATION_JSON)
fun initiateStuff(#PathParam project: String,
#PathParam path: String,
#QueryParam("completedId") completedId: String?) : StuffInitiatedResponse {
if (completedId == null) {
println("I've initiated stuff")
return StuffInitiatedResponse(UUID.randomUUID().toString(), project)
} else {
println("I've completed stuff")
return StuffCompletedResponse(completedId, project)
}
}
}
This produces what I expect but as I said, I'm annoyed that I have to repeat the class name in the "response" field of the data classes. Is there some way for me to have the class name embedded in the JSON?
The JSON library depends on the dependencies you defined. It can be either Jackson or Yasson.
I recommend using Jackson and, in this case, you can use the #JsonTypeInfo annotation on your serialized classes, which has some options to include the type in the JSON output.
I want to deserialize NASA asteroids that I get from an API call in json format like this:
data class Asteroid(
val id: Int,
val name: String = "",
val meanDiameter: Int,
)
class Deserializer : ResponseDeserializable<Asteroid> {
override fun deserialize(content: String) = Gson().fromJson(content, Asteroid::class.java)
}
How can I ignore the first top items links and page and only deserialize near_earth_objects in my Asteroid data class? And how can I access the nested items inside of near_earth_objects?
You can just ignore them.
data class NearEarthObjects(#SerializedName("near_earth_objects") val nearEarthObjects: List<Objects>)
data class Objects(val id: String, val name: String)
If you then fetch the json you can just do this:
Gson().fromJson(yourJson, NearEarthObjects::class.java)
And you will get a list of all the objects name and id.
Let's say I have the next case class:
case class Person(id: String, money: BigDecimal)
object Person {
implicit val encoder: Encoder[Person] = Encoder.forProduct2("ID", "Money")(u =>
(u.id, u.money))
I want to serialize instances of the Person class to JSON, so when I evaluate the asJson from circe, I get the result in scientific notation:
{
"ID" : "123",
"VALOR_SAP" : 2.7E+7
}
Why do this happens? I think the reason is because the default to string of BigDecimal automatically format to scientific notation.
What could I do to avoid this? May be creating another type which extends from BigDecimal and overriding the toString?
I assume that you use scala.math.BigDecimal, for java.math.BigDecimal code is similar. The way to change how objects are serialized is to provide corresponding implicit Encoder object. Unfortunately both Json and JsonNumber hierachies are sealed, so there is no very clean solution but you still can use JsonNumber.fromDecimalStringUnsafe that implements toString to just return any string you passed in. So you can do something like this:
case class Person(id: String, money: BigDecimal)
object Person {
implicit final val bigDecimalAsPlainStringEncoder: Encoder[BigDecimal] = new Encoder[BigDecimal] {
final def apply(value: BigDecimal): Json = Json.fromJsonNumber(JsonNumber.fromDecimalStringUnsafe(value.bigDecimal.toPlainString))
}
implicit val encoder: Encoder[Person] = Encoder.forProduct2("ID", "Money")(u => (u.id, u.money))
}
This question already has answers here:
How to use jackson to deserialize to Kotlin collections
(3 answers)
Closed 7 years ago.
What is the correct syntax to deserialize the following JSON:
[ {
"id" : "1",
"name" : "Blues"
}, {
"id" : "0",
"name" : "Rock"
} ]
I tried:
//Works OK
val dtos = mapper.readValue(json, List::class.java)
However I want:
val dtos : List<GenreDTO> = mapper.readValue(json,
List<GenreDTO>::class.java)
The above syntax is not correct and gives: only classes are allowed on the left hand side of a class literal
NOTE: The answer from #IRus is also correct, it was being modified at the same time I wrote this to fill in more details.
You should use the Jackson + Kotlin module or you will have other problems deserializing into Kotlin objects when you do no have a default constructor.
Your first sample of the code:
val dtos = mapper.readValue(json, List::class.java)
Is returning an inferred type of List<*> since you did not specify more type information, and it is actually a List<Map<String,Any>> which is not really "working OK" but is not producing any errors. It is unsafe, not typed.
The second code should be:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
val mapper = jacksonObjectMapper()
// ...
val genres: List<GenreDTO> = mapper.readValue(json)
You do not need anything else on the right side of the assignment, the Kotlin module for Jackson will reify the generics and create the TypeReference for Jackson internally. Notice the readValue import, you need that or .* for the com.fasterxml.jackson.module.kotlin package to have the extension functions that do all of the magic.
A slightly different alternative that also works:
val genres = mapper.readValue<List<GenreDTO>>(json)
There is no reason to NOT use the extension functions and the add-on module for Jackson. It is small and solves other issues that would require you to jump through hoops to make a default constructor, or use a bunch of annotations. With the module, your class can be normal Kotlin (optional to be data class):
class GenreDTO(val id: Int, val name: String)
The error you're getting is about following expression:
List<GenreDTO>::class.java
Because of how jvm treats generics there's no separate class for List<GenreDTO> thus compiler complains. Similarly in Java the following will not compile:
List<GenreDTO>.getClass()
Here's a sample that will deserialize the list properly:
val value:List<GenreDTO> = mapper.readValue(json, object : TypeReference<List<GenreDTO>>() {})
As #JaysonMinard has pointed out you can use jackson-module-kotlin to simplify the invocation to:
val genres: List<GenreDTO> = mapper.readValue(json)
// or
val genres = mapper.readValue<List<GenreDTO>>(json)
This is possible because of reified type parameters. Consider looking at Extensions to find out details.
Following code works well for me:
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
val json = """[ {
"id" : "1",
"name" : "Blues"
}, {
"id" : "0",
"name" : "Rock"
} ]"""
data class GenreDTO(val id: Int, val name: String)
val mapper = ObjectMapper().registerKotlinModule()
fun main(args: Array<String>) {
val obj: List<GenreDTO> = mapper.readValue(json)
obj.forEach {
println(it)
}
}
This work because of extension function defined inside jackson-kotlin-module (that used reified generics):
public inline fun <reified T: Any> ObjectMapper.readValue(content: String): T = readValue(content, object: TypeReference<T>() {})
Thanks #JaysonMinard for notify me about it.
Output:
GenreDTO(id=1, name=Blues)
GenreDTO(id=0, name=Rock)