kotlinx-serialization: how to deserialize JsonArray with diffrent kind of objects into the same Class

31 views Asked by At

I get the following JSON from an API to describe an object with different entries.

{
    "entries": [
        "Simple Text",
        {
            "type": "list",
            "items": [
                "First Item",
                "Second Item",
                "Spectral wings appear on your back, giving you a flying speed of 40 feet."
            ]
        },
        {
            "type": "entries",
            "name": "Sub-Entry",
            "entries": [
                "New text but could also be the same as above"
            ]
        }
    ]
}

In kotlin I would like to have a Entry class with a different sub-classes for one each type, so e.g. EntryText, EntryList, ...

How can i decode this into the classes since the different elements of the JsonArray are of a different type?

The problem is the a polymorphic approach would not work since the "Simple Text"-entry has no type.

1

There are 1 answers

0
Simon Jacobs On BEST ANSWER

You could use a Json transformation to alter your received JSON before normal serialization takes place.

This class:

class EntryPreSerializer : JsonTransformingSerializer<Entry>(Entry.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        return if (element is JsonPrimitive)
            buildJsonObject {
                put("type", JsonPrimitive("StringEntry"))
                put("text", element)
            }
        else element
    }
}

when applied to a serialized Entry object (our polymorphic class) will add the discriminator we need for polymorphic deserialization when it finds a JSON primitive amongst the entries.

To complete the example, here is the rest of the deserialization filled in, including showing how we apply the transformation:

@Serializable
data class EntryWrapper(val entries: List<@Serializable(with = EntryPreSerializer::class) Entry>)

@Serializable
sealed class Entry

@Serializable
data class StringEntry(val text: String) : Entry()

@Serializable @SerialName("list")
data class ListEntry(val items: List<String>) : Entry()

@Serializable @SerialName("entries")
data class EntriesEntry(val name: String, val entries: List<String>) : Entry()

Then we can test:

fun main() {
    val testJson = """
        {
            "entries": [
                "Simple Text",
                {
                    "type": "list",
                    "items": [
                        "First Item",
                        "Second Item",
                        "Spectral wings appear on your back, giving you a flying speed of 40 feet."
                    ]
                },
                {
                    "type": "entries",
                    "name": "Sub-Entry",
                    "entries": [
                        "New text but could also be the same as above"
                    ]
                }
            ]
        }
    """.trimIndent()
    println(Json.decodeFromString<EntryWrapper>(testJson))
}

Which prints:

EntryWrapper(entries=[StringEntry(text=Simple Text), ListEntry(items=[First Item, Second Item, Spectral wings appear on your back, giving you a flying speed of 40 feet.]), EntriesEntry(name=Sub-Entry, entries=[New text but could also be the same as above])])