Deserialize JSON from Riot API with kotlinx.serialization - kotlin

I have some difficulties to deserialise this JSON object from RIOT API:
{
"type":"champion",
"version":"6.1.1",
"data":{
"Thresh":{
"id":412,
"key":"Thresh",
"name":"Thresh",
"title":"the Chain Warden"
},
"Aatrox":{
"id":266,
"key":"Aatrox",
"name":"Aatrox",
"title":"the Darkin Blade"
},...
}
}
Inside the data object we have an other object with fields of all champions.
To not create all champions objects, I want de deserialise this to an list of Champion object, I expect something like that:
{
"type":"champion",
"version":"6.1.1",
"data":[
{
"id":412,
"key":"Thresh",
"name":"Thresh",
"title":"the Chain Warden"
},
{
"id":266,
"key":"Aatrox",
"name":"Aatrox",
"title":"the Darkin Blade"
},...
]
}
I think I have to create a custom Serializer that extends KSerialize but I didn't really know how to do it, can someone help me ?
On C# stackoverflow response is : Deserialize JSON from Riot API C#

There is my solution:
(If someone know witch descriptor to put there I'm interested)
object ChampionsSerializer : KSerializer<List<NetworkChampion>> {
// TODO : Not the good descriptor, fix me
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("data", kind = PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): List<NetworkChampion> {
val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
val fieldsAsJson = jsonInput.decodeJsonElement().jsonObject
val jsonParser = jsonInput.json
return fieldsAsJson.map {
jsonParser.decodeFromJsonElement(it.value)
}
}
override fun serialize(encoder: Encoder, value: List<NetworkChampion>) {
}
}
#Serializable
data class NetworkChampionsResponse(
val type: String,
val format: String,
val version: String,
#Serializable(ChampionsSerializer::class)
val data: List<NetworkChampion>
)
Json link:
https://ddragon.leagueoflegends.com/cdn/13.1.1/data/fr_FR/champion.json

Related

Jackson ContextualDeserializer cannot get the contextualType when decoding a generic Kotlin class

I have a custom implementation of a JsonDeserializer which implements a ContextualDeserializer in order to deserialize a generic class. Everything works fine until the generic class itself has a property which is also a generic class.
For demo purposes, I simplified the code to the minimum
My class is
data class MyObject<out Content>(
val content: Content
)
Deserializing MyObject<String> works:
{
"content": "Hello"
}
but deserializing MyObject<MyObject<String>>> does not:
{
"content": {
"content": "Hello"
}
}
The custom deserializer is the following. The issue seems to be that contextualType.containedType is returning null.
I believe this issue is in the line
val content = ctxt.readTreeAsValue(contentNode, contentType!!.rawClass)
because the .rawClass does not have any additional information about the generic type. So the content is deserialized as just MyObject instead of MyObject<String>.
class MyObjectDeserializer : JsonDeserializer<MyObject<*>?>(), ContextualDeserializer {
private var contentType: JavaType? = null
override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
contentType = if (property == null)
ctxt!!.contextualType.containedType(0)
else
property.type.containedType(0)
if (contentType != null) {
println("${ctxt!!.contextualType} has contained type $contentType")
} else {
// Here is where the issue occurs
println("${ctxt!!.contextualType} does not have any contained types")
}
return this
}
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext): MyObject<*> {
val codec = p?.codec ?: throw NullPointerException()
val node = codec.readTree<JsonNode>(p)
val contentNode = node.get("content")
val content = ctxt.readTreeAsValue(contentNode, contentType!!.rawClass)
return MyObject(content)
}
}
and I register the deserialzers like so
fun ObjectMapper.registerMyDeserializers(): ObjectMapper {
val module = SimpleModule().also {
it.addDeserializer(MyObject::class.java, MyObjectDeserializer())
}
return this.registerModule(module)
}
The unit test the demonstrates the issue is the follow. The first test works fine, the second fails.
class ResponseParsingTests {
private val objectMapper = jacksonObjectMapper().registerMyDeserializers()
#Test
fun `parses String as content`() {
val json = """
{
"content": "Hello"
}
""".trimIndent()
val result = objectMapper.readValue<MyObject<String>>(json)
assertEquals("Hello", result.content)
}
#Test
fun `parses MyObject as content`() {
val json = """
{
"content": {
"content": "Hello"
}
}
""".trimIndent()
val result = objectMapper.readValue<MyObject<MyObject<String>>>(json)
assertEquals("Hello", result.content.content)
}
}
Please note that I'm aware the this example class does not require any custom deserialzer at all. However my real use case is a bit more complex and I need to use a custom deserializer because I'm publishing my code as part of a library which suppoorts multiple serialization frameworks (gson, jackson, kotlinx). So the serialization cannot be part of my actual class but rather in a separate one.

Deserialize empty json value to null with kotlinx.serialization

I have the following response from a backend:
{
"title": "House",
"translations": {
"es": "Casa",
"fr": "Maison",
"de": "Haus"
}
}
To process it I am using the kotlinx serializer and this is my data class.
#Serializable
data class MyRespons(
val title: String,
val translations: Map<String,String>? = null,
)
The property translations is optional, so in some cases I can just get the title (which is fine). What the problem is, is that there also cases where the backend returns this json:
{
"title": "House",
"translations": ""
}
This throws an error because Kotlin is not converting the empty string to a null map but tries to get the properties from it. Is there a way to make Kotlin treat an empty string as if the property was not set at all? (I am trying to not make a custom serializer for this, especially because the map serializer has lots of code...)
Sadly I can't change this backend behavior and have to live with it.
you can wrap Map Serializer with:
object MapSerializer: KSerializer<Map<String,String>> {
override val descriptor: SerialDescriptor
get() = TODO("Not yet implemented")
override fun deserialize(decoder: Decoder): Map<String, String> {
if (decoder.decodeString().isEmpty())
return mapOf()
else
return MapSerializer(String.serializer(),String.serializer()).deserialize(decoder)
}
}

Kotlinx Serialization, inlining sealed class/interface [duplicate]

This question already has an answer here:
kotlinx deserialization: different types && scalar && arrays
(1 answer)
Closed 7 months ago.
With a structure similar to the following:
#Serializable
sealed class Parameters
#Serializable
data class StringContainer(val value: String): Parameters()
#Serializable
data class IntContainer(val value: Int): Parameters()
#Serializable
data class MapContainer(val value: Map<String, Parameters>): Parameters()
// more such as list, bool and other fairly (in the context) straight forward types
And the following container class:
#Serializable
data class PluginConfiguration(
// other value
val parameters: Parameters.MapContainer,
)
I want to reach a (de)serialization where the paramters are configured as a flexible json (or other) map, as one would usually expect:
{
"parameters": {
"key1": "String value",
"key2": 12,
"key3": {}
}
}
And so on. Effectively creating a flexible structure that is still structured enough to not be completely uncontrolled as Any would be. There's a fairly clearly defined (de)serialization, but I cannot figure how to do this.
I've tried reading the following
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md
And I do have a hunch that a polymorphic serializer is needed, but so far I'm bumping in to either generic structures, which I believe is way overkill for my purpose or that it for some reason cannot find the serializer for my subclasses, when writing a custom serializer for Parameters.
Update
So using custom serializers combined with surrogate classes, most things are working. The current problem is when values are put into the map, I get a kotlin.IllegalStateException: Primitives cannot be serialized polymorphically with 'type' parameter. You can use 'JsonBuilder.useArrayPolymorphism' instead. Even when I enable array polymorphism this error arises
The answer with kotlinx deserialization: different types && scalar && arrays is basically the answer, and the one I will accept. However, for future use, the complete code to my solution is as follows:
Class hierarchy
#kotlinx.serialization.Serializable(with = ParametersSerializer::class)
sealed interface Parameters
#kotlinx.serialization.Serializable(with = IntContainerSerializer::class)
data class IntContainer(
val value: Int
) : Parameters
#kotlinx.serialization.Serializable(with = StringContainerSerializer::class)
data class StringContainer(
val value: String
) : Parameters
#kotlinx.serialization.Serializable(with = MapContainerSerializer::class)
data class MapContainer(
val value: Map<String, Parameters>
) : Parameters
#kotlinx.serialization.Serializable
data class PluginConfiguration(
val plugin: String,
val parameters: MenuRunnerTest.MapContainer
)
Serializers:
abstract class BaseParametersSerializer<T : Parameters> : KSerializer<T> {
override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor
override fun serialize(encoder: Encoder, value: T) {
fun toJsonElement(value: Parameters): JsonElement = when (value) {
is IntContainer -> JsonPrimitive(value.value)
is MapContainer -> JsonObject(
value.value.mapValues { toJsonElement(it.value) }
)
is StringContainer -> JsonPrimitive(value.value)
}
val sur = toJsonElement(value)
encoder.encodeSerializableValue(JsonElement.serializer(), sur)
}
override fun deserialize(decoder: Decoder): T {
with(decoder as JsonDecoder) {
val jsonElement = decodeJsonElement()
return deserializeJson(jsonElement)
}
}
abstract fun deserializeJson(jsonElement: JsonElement): T
}
object ParametersSerializer : BaseParametersSerializer<Parameters>() {
override fun deserializeJson(jsonElement: JsonElement): Parameters {
return when(jsonElement) {
is JsonPrimitive -> when {
jsonElement.isString -> StringContainerSerializer.deserializeJson(jsonElement)
else -> IntContainerSerializer.deserializeJson(jsonElement)
}
is JsonObject -> MapContainerSerializer.deserializeJson(jsonElement)
else -> throw IllegalArgumentException("Only ints, strings and strings are allowed here")
}
}
}
object StringContainerSerializer : BaseParametersSerializer<StringContainer>() {
override fun deserializeJson(jsonElement: JsonElement): StringContainer {
return when(jsonElement) {
is JsonPrimitive -> StringContainer(jsonElement.content)
else -> throw IllegalArgumentException("Only strings are allowed here")
}
}
}
object IntContainerSerializer : BaseParametersSerializer<IntContainer>() {
override fun deserializeJson(jsonElement: JsonElement): IntContainer {
return when (jsonElement) {
is JsonPrimitive -> IntContainer(jsonElement.int)
else -> throw IllegalArgumentException("Only ints are allowed here")
}
}
}
object MapContainerSerializer : BaseParametersSerializer<MapContainer>() {
override fun deserializeJson(jsonElement: JsonElement): MapContainer {
return when (jsonElement) {
is JsonObject -> MapContainer(jsonElement.mapValues { ParametersSerializer.deserializeJson(it.value) })
else -> throw IllegalArgumentException("Only maps are allowed here")
}
}
}
This structure should be expandable for lists, doubles and other structures, not included in the example :)

Serialize enum field into JSON in Kotlin

I've got a stupid question that stunned me a bit.
I have an enum and a data class like this:
enum class MyEventType(val typeName: String) {
FIRST("firstEventReceived")
}
data class MyEvent(
val id: String,
val event: MyEventType
)
I need to send this as a json string, but common desearilizer makes such a json
{
"id": "identifier",
"event": "FIRST"
}
but i need
{
"id": "identifier",
"event": "firstEventReceived"
}
As far as i understand, kotlin allows to override getter in data class, but i didn't succeed in it... Trying to make
data class MyEvent(
val id: String
) {
val event: MyEventType get() event.typeName
}
but i've missed something, i guess...
The simplest way is probably to annotate the property with #JsonValue:
enum class MyEventType(#JsonValue val typeName: String) {
FIRST("firstEventReceived")
}
data class MyEvent(
val id: String,
val event: MyEventType
)
fun main() {
MyEvent(id = "foo", event = MyEventType.FIRST)
.let { jacksonObjectMapper().writeValueAsString(it) }
.let { println(it) }
}
Prints:
{"id":"foo","event":"firstEventReceived"}
The easiest way is to annotate the typeName with #JsonValue. This will serialise and deserialise the enum field as you want.
enum class MyEventType(#JsonValue val typeName: String) {
FIRST("firstEventReceived");
}
An alternative is to use #JsonFormat (if you are using jackson version < 2.9);
enum class MyEventType(#JsonFormat(shape = JsonFormat.Shape.OBJECT) val typeName: String) {
FIRST("firstEventReceived");
}
Herer's an example;
#JvmStatic
fun main(args: Array<String>) {
val mapper = jacksonObjectMapper()
val json = mapper.writeValueAsString(MyEvent("1", MyEventType.FIRST))
println(json)
val event = mapper.readValue<MyEvent>(json)
println(event)
}
You get the output;
{"id":"1","event":"firstEventReceived"}
MyEvent(id=1, event=FIRST)
I used Jackson version 2.12.0. Here's a good read on enum manipulation with Jackson - https://www.baeldung.com/jackson-serialize-enums
Also you can have enum with 2+ fields which you want to be serialized
enum class MyEventType(
val firstField: String,
val secondField: String,
val thirdField: String
) {
MY_ENUM("firstFieldValue", "secondFieldValue", "thirdFieldValue")
}
You can chose one of the following two options:
Put #JsonValue over a method(lets call it getter) that will return the required value(if you need only part of the fields):
#JsonValue
fun getSerializedObject(): String {
return "{firstField: $firstField, thirdField: $thirdField}"
}
Result will be "{firstField: firstFieldValue, thirdField: thirdFieldValue}"
Put #JsonFormat(shape = JsonFormat.Shape.OBJECT) over your enum class(for serialization class as common class):
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
enum class MyEventType(
val firstField: String,
val secondField: String,
val thirdField: String
) {
MY_ENUM("firstField", "secondField", "thirdField")
}
Result will be "{"firstField": "firstFieldValue", "secondField": "secondFieldValue", "thirdField": "thirdFieldValue"}"
For GSON users, you can use the #SerializedName annotation:
enum class ConnectionStatus {
#SerializedName("open")
OPEN,
#SerializedName("connecting")
CONNECTING,
#SerializedName("closed")
CLOSED
}

Custom serializer with polymorphic kotlinx serialization

With kotlinx.serialization polymorphism, I want to get
{"type":"veh_t","owner":"Ivan","bodyType":"cistern","carryingCapacityInTons":5,"detachable":false}
but I get
{"type":"kotlin.collections.LinkedHashMap","owner":"Ivan","bodyType":"cistern","carryingCapacityInTons":5,"detachable":false}
I use the following models
interface Vehicle {
val owner: String
}
#Serializable
#SerialName("veh_p")
data class PassengerCar(
override val owner: String,
val numberOfSeats: Int
) : Vehicle
#Serializable
#SerialName("veh_t")
data class Truck(
override val owner: String,
val body: Body
) : Vehicle {
#Serializable
data class Body(
val bodyType: String,
val carryingCapacityInTons: Int,
val detachable: Boolean
//a lot of other fields
)
}
I apply the following Json
inline val VehicleJson: Json get() = Json(context = SerializersModule {
polymorphic(Vehicle::class) {
PassengerCar::class with PassengerCar.serializer()
Truck::class with TruckKSerializer
}
})
I use serializer TruckKSerializer because the server adopts a flat structure. At the same time, in the application I want to use an object Truck.Body. For flatten I override fun serialize(encoder: Encoder, obj : T) and fun deserialize(decoder: Decoder): T in Serializator using JsonOutput and JsonInput according to the documentation in these classes.
object TruckKSerializer : KSerializer<Truck> {
override val descriptor: SerialDescriptor = SerialClassDescImpl("Truck")
override fun serialize(encoder: Encoder, obj: Truck) {
val output = encoder as? JsonOutput ?: throw SerializationException("This class can be saved only by Json")
output.encodeJson(json {
obj::owner.name to obj.owner
encoder.json.toJson(Truck.Body.serializer(), obj.body)
.jsonObject.content
.forEach { (name, value) ->
name to value
}
})
}
#ImplicitReflectionSerializer
override fun deserialize(decoder: Decoder): Truck {
val input = decoder as? JsonInput
?: throw SerializationException("This class can be loaded only by Json")
val tree = input.decodeJson() as? JsonObject
?: throw SerializationException("Expected JsonObject")
return Truck(
tree.getPrimitive("owner").content,
VehicleJson.fromJson<Truck.Body>(tree)
)
}
}
And finally, I use stringify(serializer: SerializationStrategy<T>, obj: T)
VehicleJson.stringify(
PolymorphicSerializer(Vehicle::class),
Truck(
owner = "Ivan",
body = Truck.Body(
bodyType = "cistern",
carryingCapacityInTons = 5,
detachable = false
)
)
)
I end up with {"type":"kotlin.collections.LinkedHashMap", ...}, but I need {"type":"veh_t", ...}
How do I get the right type? I want using polymorphism for Vehicle and encode Body object with Truck.Body.serializer() to flatten.
With this serialization, the PassengerCar class runs fine.
VehicleJson.stringify(
PolymorphicSerializer(Vehicle::class),
PassengerCar(
owner = "Oleg",
numberOfSeats = 4
)
)
Result is correct:
{"type":"veh_p","owner":"Oleg","numberOfSeats":4}
I think the problem is the custom serializer TruckKSerializer.
And I noticed if I use in my overridden fun serialize(encoder: Encoder, obj : T) next code
encoder
.beginStructure(descriptor)
.apply {
//...
}
.endStructure(descriptor)
I get the correct type but cannot flatten the object Truck.Body using its serializer.
the correct way to open and close a composite {}
is this code
val composite = encoder.beginStructure(descriptor)
// use composite instead of encoder here
composite.endStructure(descriptor)
and you should be able to serialize Body using .encodeSerializable(Body.serializer(), body)
and always pass the descriptor along otherwise it will fall back to stuff like that LinkedhashMap for the json dictionary