Polymorphic serialization of sealed hierarchies with generic type parameters

806 views Asked by At

Using Kotlin serialization, I would like to serialize and deserialize (to JSON) a generic data class with type parameter from a sealed hierarchy. However, I get a runtime exception.

To reproduce the issue:

import kotlinx.serialization.*
import kotlin.test.Test
import kotlin.test.assertEquals

/// The sealed hierarchy used a generic type parameters:
@Serializable
sealed interface Coded {
    val description: String
}

@Serializable
@SerialName("CodeOA")
object CodeOA: Coded {
    override val description: String = "Code Object OA"
}

@Serializable
@SerialName("CodeOB")
object CodeOB: Coded {
    override val description: String = "Code Object OB"
}


/// Simplified class hierarchy
@Serializable
sealed interface NumberedData {
    val number: Int
}

@Serializable
@SerialName("CodedData")
data class CodedData<out C : Coded> (
    override val number: Int,
    val info: String,
    val code: C
): NumberedData

internal class GenericSerializerTest {
    @Test
    fun `polymorphically serialize and deserialize a CodedData instance`() {
        val codedData: NumberedData = CodedData(
            number = 42,
            info = "Some test",
            code = CodeOB
        )
        val codedDataJson = Json.encodeToString(codedData)
        val codedDataDeserialized = Json.decodeFromString<NumberedData>(codedDataJson)
        assertEquals(codedData, codedDataDeserialized)
    }
}

Running the test results in the following runtime exception:

kotlinx.serialization.SerializationException: Class 'CodeOB' is not registered for polymorphic serialization in the scope of 'Coded'.
Mark the base class as 'sealed' or register the serializer explicitly.

This error message does not make sense to me, as both hierarchies are sealed and marked as @Serializable.

I don't understand the root cause of the problem - do I need to explicitly register one of the plugin-generated serializers? Or do I need to roll my own serializer? Why would that be the case?

I am using Kotlin 1.7.20 with kotlinx.serialization 1.4.1

1

There are 1 answers

1
amanin On BEST ANSWER

Disclaimer: I do not consider my solution to be very statisfying, but I cannot find a better way for now.

KotlinX serialization documentation about sealed classes states (emphasis mine):

you must ensure that the compile-time type of the serialized object is a polymorphic one, not a concrete one.

In the following example of the doc, we see that serializing child class instead of parent class prevent it to be deserialized using parent (polymorphic) type.

In your case, you have nested polymorphic types, so this is even more complicated I think. To make serialization and deserialization work, then, I've tried multiple things, and finally, the only way I've found to make it work is to:

  1. Remove generic on CodedData (to be sure that code attribute is interpreted in a polymorphic way:
@Serializable
@SerialName("CodedData")
data class CodedData (
    override val number: Int,
    val info: String,
    val code: Coded
): NumberedData
  1. Cast coded data object to NumberedData when encoding, to ensure polymorphism is triggered:
Json.encodeToString<NumberedData>(codedData)

Tested using a little main program based on your own unit test:

fun main() {
    val codedData = CodedData(
        number = 42,
        info = "Some test",
        code = CodeOB
    )
    val json = Json.encodeToString<NumberedData>(codedData)
    println(
        """
            ENCODED:
            --------
            $json
        """.trimIndent()
    )

    val decoded = Json.decodeFromString<NumberedData>(json)
    println(
        """
            DECODED:
            --------
            $decoded
        """.trimIndent()
    )
}

It prints:

ENCODED:
--------
{"type":"CodedData","number":42,"info":"Some test","code":{"type":"CodeOB"}}
DECODED:
--------
CodedData(number=42, info=Some test, code=CodeOB(description = Code Object OB))