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?
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
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
Most helpful comment
This is a version of
PolymorphicSerializerwhich works in JS and JVM:In your common code you need something like:
You can use them equally in JVM and JS: