jsonkotlindeserializationkotlinx.serialization

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


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.


Solution

  • 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])])