Search code examples
jsonkotlinkotlinx.serializationkotlinx

kotlinx serialization — best way to do polymorphic child deserialization


I have a Json input like:

{
   "type": "type_1",
   "data": {
      // ...
   }
}

data field can vary depending on type.

So, I need a deserializer, that looks on type (enum) and deserializes data respectively (for instance, for type_1 value it's Type1 class, for type_2Type2, etc).

I thought about a fully-custom deserializer (extending a KSerializer<T>), but it looks like an overkill.

What's the best (kotlin) way to do such deserialization?


Solution

  • Kotlin way for polymorphic deserialization is to have a plain JSON (with all data fields on the same level as type field):

    {
       "type": "type_1",
       // ...
    }
    

    and register all subclasses of abstract superclass with serializers module (this step could be skipped if superclass is a sealed class).

    No need for enums - just mark subclasses declarations with respectful @SerialName("type_1") annotations if its name in JSON differs from fully-qualified class name.

    If original JSON shape is a strict requirement, then you may transform it on the fly to a plain one, reducing the task to the previous one.

    @Serializable(with = CommonAbstractSuperClassDeserializer::class)
    abstract class CommonAbstractSuperClass
    
    @Serializable
    @SerialName("type_1")
    data class Type1(val x: Int, val y: Int) : CommonAbstractSuperClass()
    
    @Serializable
    @SerialName("type_2")
    data class Type2(val a: String, val b: Type1) : CommonAbstractSuperClass()
    
    object CommonAbstractSuperClassDeserializer :
        JsonTransformingSerializer<CommonAbstractSuperClass>(PolymorphicSerializer(CommonAbstractSuperClass::class)) {
        override fun transformDeserialize(element: JsonElement): JsonElement {
            val type = element.jsonObject["type"]!!
            val data = element.jsonObject["data"] ?: return element
            return JsonObject(data.jsonObject.toMutableMap().also { it["type"] = type })
        }
    }
    
    fun main() {
        val kotlinx = Json {
            serializersModule = SerializersModule {
                polymorphic(CommonAbstractSuperClass::class) {
                    subclass(Type1::class)
                    subclass(Type2::class)
                }
            }
        }
    
        val str1 = "{\"type\":\"type_1\",\"data\":{\"x\":1,\"y\":1}}"
        val obj1 = kotlinx.decodeFromString<CommonAbstractSuperClass>(str1)
        println(obj1) //Type1(x=1, y=1)
        val str2 = "{\"type\":\"type_2\",\"data\":{\"a\":\"1\",\"b\":{\"x\":1,\"y\":1}}}"
        val obj2 = kotlinx.decodeFromString<CommonAbstractSuperClass>(str2)
        println(obj2) //Type2(a=1, b=Type1(x=1, y=1))
    
        //Works for plain JSON shape as well:
        val str0 = "{\"type\":\"type_1\",\"x\":1,\"y\":1}"
        val obj0 = kotlinx.decodeFromString<CommonAbstractSuperClass>(str0)
        println(obj0) //Type1(x=1, y=1)
    }