kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Incorrecr generic sealed-class polymorhic serializer plugin

Open sergeych opened this issue 1 year ago • 6 comments

The following code fails on compilation:

    @Serializable
    sealed class CTest<A> {
        abstract val c: Collection<A>

        @Serializable
        class AsList<A>(override val c: List<A>) : CTest<A>()
    }

    @Test
    fun testSerialization() {
        val x: CTest<Int> = CTest.AsList(
            listOf(1,2,3)
        )
        println(Json.encodeToString(x))

with strange error text:

Serializer for subclass 'Int' is not found in the polymorphic scope of 'Any'.
Check if class with serial name 'Int' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'Int' has to be '@Serializable', and the base class 'Any' has to be sealed and '@Serializable'.
kotlinx.serialization.SerializationException: Serializer for subclass 'Int' is not found in the polymorphic scope of 'Any'.
Check if class with serial name 'Int' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'Int' has to be '@Serializable', and the base class 'Any' has to be sealed and '@Serializable'.

To Reproduce

code snippet above.

Expected behavior

either correct serialization or correct error message. To be clear: Int serializer exists, and should not be requested to be re-declared in anyway, especially in the Any, and Any should not be requested to be made sealed ;)

If such serialization can't be done automatically, please describe how it can be done by hand; i tried CTest.serializer(Int...) but it did not help with the same error.

Environment

  • Kotlin version: 2.0.0
  • Library version: 1.7.1
  • Kotlin platforms:JVM tested but this seems to be plugin problem this platform independent
  • Gradle version: 8.1

sergeych avatar Jun 26 '24 00:06 sergeych

In general, we do not support serialization to/from Any, even for basic types. In contrast with most frameworks, Json.encodeToString(listOf<Any>(1, "a", listOf("b")) produces the exception instead of [1, "a", ["b"]]. Exception you see here is aimed to describe exactly this: Int is not expected to be serialized as a part of Collection<Any>. You can try to get rid of generics and use List<Int> directly, or use JsonElements to work with untyped Json tree. See also: #296

sandwwraith avatar Jul 05 '24 16:07 sandwwraith

I do not mean use Any. I use generic parameter, very well known at the time of usage. It actually works if I won't use sealed class polymorphism, in the code above, serializing still-generic type CTest.AsList<A> works as expected:

  val y = CTest.AsList(listOf(1,2,3))
  println(Json.encodeToString(y)) //> {"c":[1,2,3]}

Why it works with CTest.AsList' but does not work if we use its sealed parent, CTest? And why it says "Any" while it is actually Int type?List<Int>` serializes pretty well, also in the example just above.

sergeych avatar Jul 05 '24 18:07 sergeych

@sergeych I think I know what is going on. The type of x is CTest<A> where A is a generic type that is bounded by Any (this is where the any comes from. The serializer for CTest is a sealed class serializer, that will delegate serialization to its child. However, when you look at SealedClassSerializer in the library it is clear that this does not support type parameters. Instead resolution falls back to regular polymorphic resolution using the module. That resolution then considers the bound for A in AsList (Any) and tries to find a registered serializer for the subtype Int (this is not registered, thus the error).

What would need to happen is for the plugin to generate the SealedClassSerializer to contain the specialised children appropriately (and maybe do some generic magic with the baseclass' type parameter).

pdvrieze avatar Jul 05 '24 20:07 pdvrieze

@pdvrieze I think you are right, it explains everything.

Dear maintainers, please fix the plugin to properly serialize generic sealed-class polymorphism? Right now it is not working with generic types which is not good. We need it to make real use of the kotinx serialization. Which is by far the only maintained KMP serialization I know.

sergeych avatar Jul 06 '24 06:07 sergeych

I think it was already discussed in #2555

In short, A in CTest<A> and A in class AsList<A> are two completely different types. They're not related to each other. Even if we store ASerializer in CTest, it doesn't mean we can successfully deserialize AsList when we see Json like {"type": "AsList", "c":[1,2,3]} — because there is no information on what type arguments of AsList are. I can recommend you to use subtypes without type parameters:

@Serializable
sealed class CTest<A> {
    abstract val c: Collection<A>

    @Serializable
    class IntList(override val c: List<Int>) : CTest<Int>()
}

sandwwraith avatar Jul 08 '24 10:07 sandwwraith

I think it was already discussed in #2555

In short, A in CTest<A> and A in class AsList<A> are two completely different types. They're not related to each other. Even if we store ASerializer in CTest, it doesn't mean we can successfully deserialize AsList when we see Json like {"type": "AsList", "c":[1,2,3]} — because there is no information on what type arguments of AsList are. I can recommend you to use subtypes without type parameters:

@Serializable
sealed class CTest<A> {
    abstract val c: Collection<A>

    @Serializable
    class IntList(override val c: List<Int>) : CTest<Int>()
}

When I'm calling deserializer, I provide it with a specific type: Format.decode<CTyle<Int>>(...). At this point well-designed plugin can provide decoder with actual types across the sealed hierarchy.

Even if it can't, the klutch could be used to specify serializer subtypes manually, like

   Format.decode(CTyle.seriializer( Int.serializer),...)

It is ugly but better than nothing. Insofar sealed-class polymorphism with generic types is not working further than samples, and when I've tried to write own intelligent serializer for close-type hierarchies I found that most of it mechanics is concealed and not available outside. Not to mention that it is very sparsely documented.

So, if you see no way to improve the plugin (the point where you know for sure actual types), maybe you'll leave us a way to specify types by hand?

sergeych avatar Jul 12 '24 14:07 sergeych

Relates #1736

shanshin avatar Feb 10 '25 14:02 shanshin

I tested my similar use case (sealed interface with type parameter used in abstract property). It is working perfectly well with Kotlin 2.2.0-RC!

plugins {
    kotlin("jvm") version "2.2.0-RC"
    kotlin("plugin.serialization") version "2.2.0-RC"
}

The same code failed with Kotlin 2.1.20. Thank you very much!

dlkw avatar May 30 '25 15:05 dlkw