Polymorphic serialization doesn't work for deeper sealed class hierarchies

I have the following code:

@Serializable
sealed class Component : Tagged<UUID, Component> {

    @Serializable @SerialName("name")
    data class Name(val name: String, @Transient override val tag: Tag<Component> = Tag.Name) : Component()

    @Serializable @SerialName("parent")
    data class Parent(val parent: UUID, @Transient  override val tag: Tag<Component> = Tag.Parent) : Component()

    @Serializable @SerialName("entity_type")
    sealed class EntityType : Component() {
        @Transient override val tag = Tag.EntityType

        @Serializable @SerialName("department") object Department : EntityType()
        @Serializable @SerialName("function")  object Function : EntityType()
    }

    @Serializable @SerialName("ordered_children")
    data class OrderedChildren(val children: List<UUID> = listOf(), override val tag: Tag<Component> = Tag.OrderedChildren) : Component()
}

However, when serializing I get the following error:

Class 'Department' is not registered for polymorphic serialization in the scope of 'Component'.
Mark the base class as 'sealed' or register the serializer explicitly.

Why is that? Because ‘Department’ is clearly in the sealed class hierarchy.

This works though:

 val module = SerializersModule {
            polymorphic(Component::class) {
                subclass(Component.EntityType.Department::class)
                subclass(Component.EntityType.Function::class)
            }
        }

but that seems unnecessary to me if I have a strictly sealed hierarchy.

6 Likes

This seems to be a shortcoming/bug(missing feature) in the library (or format), where it doesn’t handle nested sealed types.

@avwie , I’ve just run into the same issue, it still exists in the latest version :neutral_face:
Thanks for the workaround, it fixed the problem :slight_smile:

Just stumbled into this issue as well. I was thinking on writing a custom JsonContentPolymorphicSerializer for that. I will try the workaround. Want to be subscribed to this topic.

So, my case is a bit different. Here is my class structure. Check the inclusion of JsonClassDiscriminator in the base class Component and also in nested base class EntityType. The fields type and sub_type both will be present in the sub classes of EntityType. I get an error on nested base class saying

Argument values for inheritable serial info annotation 'JsonClassDiscriminator' must be the same as the values in parent type 'Component'

is there any workarounds for this issue?

@Serializable
@JsonClassDiscriminator("base_type")
sealed class Component : Tagged<UUID, Component> {

    @Serializable @SerialName("name")
    data class Name(val name: String, @Transient override val tag: Tag<Component> = Tag.Name) : Component()

    @Serializable @SerialName("parent")
    data class Parent(val parent: UUID, @Transient  override val tag: Tag<Component> = Tag.Parent) : Component()

    @Serializable
    **@JsonClassDiscriminator("sub_type")**
    sealed class EntityType : Component() {
        @Serializable @SerialName("department") data class Department : EntityType()
        @Serializable @SerialName("function")  data class Function : EntityType()
    }

    @Serializable @SerialName("ordered_children")
    data class OrderedChildren(val children: List<UUID> = listOf(), override val tag: Tag<Component> = Tag.OrderedChildren) : Component()
}

Hi guys I have encountered with the same need but when looking up at the docs I have found a solution

I believe that the following will resolve the issue, but we will have to insert each concrete class manually. (not using the magic of sealed subclasses)

object A {

    interface Base


    interface SubBase : Base
    @Serializable
    data class Concrete(
        val name: String
    ) : SubBase


    @Serializable
    sealed interface Sealed : Base
    @Serializable
    data class ConcreteSealed(
        val name: String
    ) : Sealed


    @JvmStatic
    fun main(arr: Array<String>) {
        val module = SerializersModule {
            fun PolymorphicModuleBuilder<Concrete>.registerConcrete() {
                subclass(Concrete::class)
            }
            fun PolymorphicModuleBuilder<ConcreteSealed>.registerConcreteSealed() {
                subclass(ConcreteSealed::class)
            }
            polymorphic(Base::class) {
                registerConcrete()
                registerConcreteSealed()
            }
            polymorphic(SubBase::class) {
                registerConcrete()
            }
        }
        val json = Json {
            serializersModule = module
        }
        val base: Base = Concrete("itay")
        println("encode base: " + json.encodeToString(base)) // Will use our custom polymorphic serializer

        val subBase: SubBase = Concrete("itay")
        println("encode subBase: " + json.encodeToString(subBase)) // Will use our custom polymorphic serializer

        val sealed: Sealed = ConcreteSealed("itay")
        println("encode sealed: " + json.encodeToString(sealed)) // Will use the default sealed serializer
    }
}

will print:

encode base: {"type":"com.example.you.A.Concrete","name":"itay"}
encode subBase: {"type":"com.example.you.A.Concrete","name":"itay"}
encode sealed: {"type":"com.example.you.A.ConcreteSealed","name":"itay"}