Kotlinx.serialization: How to: serializing / deserializing sealed classes

Created on 24 Mar 2018  路  6Comments  路  Source: Kotlin/kotlinx.serialization

Let's assume we have a simple (sealed) hierarchy of types

@Serializable
sealed class A

@Serializable
data class B(val a : Int, val b: String) : A()

@Serializable
data class C(val c : Boolean, val d: Boolean) : B() 

Now we want to serialize instances (example: JSON):

fun toJSON( x : A ) {
    println( JSON.stringify(x) ) //  == "{}" because A's serializer handles a class without fields 
}

fun fromJSON(json: String) {
   return JSON.parse<A>(json) // runtime error (?)
}

Of course this does not work, but how would I get something like this to work? Especially sharing between JVM and JS?

One solution is of course to build different data types via Composition instead of Inheritance, but I find this option to be something of a code smell in cases like this.

I'm somewhat reminded of Scala's upickle, which is based on Macro based de-/serializers, with kotlinx.serialization doing something similar via preprocessing by compiler / build tool plugin (AFAIU). With sealed classes being compile time constants, this should definitly work in theory.

Is there a way to do this more elegantly, keeping the sealed class hierarchy intact?

feature

Most helpful comment

This is a version of PolymorphicSerializer which works in JS and JVM:

import kotlinx.serialization.KInput
import kotlinx.serialization.KOutput
import kotlinx.serialization.KSerialClassDesc
import kotlinx.serialization.KSerialClassKind
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.internal.SerialClassDescImpl
import kotlinx.serialization.serializer
import kotlin.reflect.KClass


fun <T : Any> serializationModel(vararg serializables: KClass<out T>) = SerializationModel(serializables)

class SerializationModel<T : Any>(val serializables: Array<out KClass<out T>>)

class ModelSerializer<T : Any>(private val model: SerializationModel<T>) : KSerializer<T> {
    override val serialClassDesc: KSerialClassDesc
        get() = PolymorphicClassDesc

    override fun save(output: KOutput, obj: T) {
        val saver = obj::class.serializer() as KSerializer<T>
        @Suppress("NAME_SHADOWING")
        val output = output.writeBegin(serialClassDesc)
        output.writeIntElementValue(serialClassDesc, 0, model.serializables.indexOf(obj::class))
        output.writeSerializableElementValue(serialClassDesc, 1, saver, obj)
        output.writeEnd(serialClassDesc)
    }

    override fun load(input: KInput): T {
        @Suppress("NAME_SHADOWING")
        val input = input.readBegin(serialClassDesc)
        var klassIndex: Int? = null
        var value: T? = null
        mainLoop@ while (true) {
            when (input.readElement(serialClassDesc)) {
                KInput.READ_ALL -> {
                    klassIndex = input.readIntElementValue(serialClassDesc, 0)
                    val loader = model.serializables[klassIndex].serializer()
                    value = input.readSerializableElementValue(serialClassDesc, 1, loader)
                    break@mainLoop
                }
                KInput.READ_DONE -> {
                    break@mainLoop
                }
                0 -> {
                    klassIndex = input.readIntElementValue(serialClassDesc, 0)
                }
                1 -> {
                    klassIndex = requireNotNull(klassIndex) { "Cannot read polymorphic value before its type token" }
                    val loader = model.serializables[klassIndex].serializer()
                    value = input.readSerializableElementValue(serialClassDesc, 1, loader)
                }
                else -> throw SerializationException("Invalid index")
            }
        }

        input.readEnd(serialClassDesc)
        return requireNotNull(value) { "Polymorphic value have not been read" }
    }
}

internal object PolymorphicClassDesc : SerialClassDescImpl("kotlin.Any") {
    override val kind: KSerialClassKind = KSerialClassKind.POLYMORPHIC

    init {
        addElement("klass")
        addElement("value")
    }
}

In your common code you need something like:

sealed class LoginRequestEvent {
    @Serializable
    data class ReuseSession(val sessionId: String) : LoginRequestEvent()

    @Serializable
    data class CreateSession(val username: String, val password: String) : LoginRequestEvent()
}

val model = serializationModel(
    LoginRequestEvent.ReuseSession::class,
    LoginRequestEvent.CreateSession::class
)

You can use them equally in JVM and JS:

val serializer = ModelSerializer(model) // import model from common code

val loginRequest = JSON.parse<LoginRequestEvent>(serializer, payload)

when(loginRequest) {
    is LoginRequestEvent.ReuseSession -> [...]

All 6 comments

I ran into the same problem while trying to build some typed JVM/JS messaging. In JVM there is a PolymorphicSerializer but due to missing reflection in JS, there is no equivalent unfortunately.

What I did is the following: Provide a list of all possible classes (as KClass objects) in common code and a wrapper class which holds the following data:

  1. index of class of serialized object in that list
  2. object serialized as String.

Now I can deserialize the wrapper, which always has the same class then look up the real class in the list, geht the serializer from it and deserialize the real object. Cast it to the base class and you can do a when on it.

I know this is ugly and inefficient, but I didn't find a better solution yet.

Sealed classes don't have any kind of special support now, but usual PolymorphicSerializer should work fine for them.

On JS, I think it is possible to implement custom deserializer for base sealed class, using PolymorphicSerializer's code as a reference; but instead of loading class for name and obtaining serializer do something like

val type = readString (...) // 1st element of array
when(type) {
    "B" -> readSerializableElement(B.serializer(), ...)
    "C" -> readSerializableElement(C.serializer(),...)
// others...
}

I think when this feature would be supported by plugin, it would generate code similar to that

Tried something similar to @sandwwraith but was not successful perhaps I'm doing something foolish

    override fun load(input: KInput): Item {
       val mapOfStrings = (StringSerializer to StringSerializer).map
       return input.read(mapOfStrings)
               .let {
                   JSON.parse(
                           CdItemTypes.valueOf(it["type"]!!).itemClass().serializer(),
                           JSON.stringify(mapOfStrings, it)
                   )
               }
   }

"Any type is not supported"

@sandwwraith Can you shed more light on this, I am facing a similar issue, trying to serialize JSON for sealed classes and get the following error:

Expected '[, kind: POLYMORPHIC'

I have classes that look like this:
@Serializable sealed class Base { abstract val id: String @Serializable data class Extended( override val id: String ): Base()

This is a version of PolymorphicSerializer which works in JS and JVM:

import kotlinx.serialization.KInput
import kotlinx.serialization.KOutput
import kotlinx.serialization.KSerialClassDesc
import kotlinx.serialization.KSerialClassKind
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.internal.SerialClassDescImpl
import kotlinx.serialization.serializer
import kotlin.reflect.KClass


fun <T : Any> serializationModel(vararg serializables: KClass<out T>) = SerializationModel(serializables)

class SerializationModel<T : Any>(val serializables: Array<out KClass<out T>>)

class ModelSerializer<T : Any>(private val model: SerializationModel<T>) : KSerializer<T> {
    override val serialClassDesc: KSerialClassDesc
        get() = PolymorphicClassDesc

    override fun save(output: KOutput, obj: T) {
        val saver = obj::class.serializer() as KSerializer<T>
        @Suppress("NAME_SHADOWING")
        val output = output.writeBegin(serialClassDesc)
        output.writeIntElementValue(serialClassDesc, 0, model.serializables.indexOf(obj::class))
        output.writeSerializableElementValue(serialClassDesc, 1, saver, obj)
        output.writeEnd(serialClassDesc)
    }

    override fun load(input: KInput): T {
        @Suppress("NAME_SHADOWING")
        val input = input.readBegin(serialClassDesc)
        var klassIndex: Int? = null
        var value: T? = null
        mainLoop@ while (true) {
            when (input.readElement(serialClassDesc)) {
                KInput.READ_ALL -> {
                    klassIndex = input.readIntElementValue(serialClassDesc, 0)
                    val loader = model.serializables[klassIndex].serializer()
                    value = input.readSerializableElementValue(serialClassDesc, 1, loader)
                    break@mainLoop
                }
                KInput.READ_DONE -> {
                    break@mainLoop
                }
                0 -> {
                    klassIndex = input.readIntElementValue(serialClassDesc, 0)
                }
                1 -> {
                    klassIndex = requireNotNull(klassIndex) { "Cannot read polymorphic value before its type token" }
                    val loader = model.serializables[klassIndex].serializer()
                    value = input.readSerializableElementValue(serialClassDesc, 1, loader)
                }
                else -> throw SerializationException("Invalid index")
            }
        }

        input.readEnd(serialClassDesc)
        return requireNotNull(value) { "Polymorphic value have not been read" }
    }
}

internal object PolymorphicClassDesc : SerialClassDescImpl("kotlin.Any") {
    override val kind: KSerialClassKind = KSerialClassKind.POLYMORPHIC

    init {
        addElement("klass")
        addElement("value")
    }
}

In your common code you need something like:

sealed class LoginRequestEvent {
    @Serializable
    data class ReuseSession(val sessionId: String) : LoginRequestEvent()

    @Serializable
    data class CreateSession(val username: String, val password: String) : LoginRequestEvent()
}

val model = serializationModel(
    LoginRequestEvent.ReuseSession::class,
    LoginRequestEvent.CreateSession::class
)

You can use them equally in JVM and JS:

val serializer = ModelSerializer(model) // import model from common code

val loginRequest = JSON.parse<LoginRequestEvent>(serializer, payload)

when(loginRequest) {
    is LoginRequestEvent.ReuseSession -> [...]

Closed with #572. Sealed classes have auto-polymorphism in 0.14.0

Was this page helpful?
0 / 5 - 0 ratings