Incorrecr generic sealed-class polymorhic serializer plugin
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
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
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 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 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.
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>()
}
I think it was already discussed in #2555
In short, A in
CTest<A>and A inclass 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 deserializeAsListwhen we see Json like{"type": "AsList", "c":[1,2,3]}— because there is no information on what type arguments ofAsListare. 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?
Relates #1736
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!