2.0.20 with generatedSerializer and reference to interface: SealedClassSerializer init: subclassSerializers contains null
Describe the bug
java.lang.NullPointerException
at kotlinx.serialization.SealedClassSerializer$special$$inlined$groupingBy$1.keyOf(_Collections.kt:1547)
at kotlinx.serialization.SealedClassSerializer.<init>(SealedSerializer.kt:158)
at kotlinx.serialization.SealedClassSerializer.<init>(SealedSerializer.kt:97)
This only happens if a child has a reference to the parent interface, in this case SealedClassSerializer.subclassSerializers has nullable entries, but it passes the init check because the array has the same length.
To Reproduce
@Serializable
public sealed interface TestSchema
@Serializable(with = Bar.Companion.CustomSerializer::class)
@SerialName("bar")
@KeepGeneratedSerializer
data class Bar(val bar: Int) : TestSchema {
companion object {
internal object CustomSerializer : KSerializer<Bar> by generatedSerializer() // just a dummy for the test
}
}
@Serializable(with = ASDF.Companion.CustomSerializer::class)
@SerialName("asdf")
@KeepGeneratedSerializer
data class ASDF(
val child: TestSchema,
) : TestSchema {
companion object {
internal object CustomSerializer : KSerializer<ASDF> by generatedSerializer()
}
}
@Test
fun internalError() {
val (schema, schema2) = Json.decodeFromString(ListSerializer(TestSchema.serializer()), """
[
{
"type": "bar",
"bar": 42,
},
{
"type": "asdf",
"child": {
"type": "bar",
"bar": 42
}
}
]
""")
assertTrue(schema is Bar)
assertEquals(42, schema.bar)
assertTrue(schema2 is ASDF)
val child = schema2.child
assertTrue(child is Bar)
assertEquals(42, child.bar)
}
// Just nice to add it to your internal tests to not break support for polymorphicDefaultDeserializer
@Test
fun internalErrorWithDefault() {
val (schema, schema2) = Json {
serializersModule = SerializersModule {
polymorphicDefaultDeserializer(TestSchema::class) {
ASDF.Companion.CustomSerializer
}
}
}.decodeFromString(ListSerializer(TestSchema.serializer()), """
[
{
"type": "bar",
"bar": 42
},
{
"child": {
"type": "bar",
"bar": 42
}
}
]
""")
assertTrue(schema is Bar)
assertEquals(42, schema.bar)
assertTrue(schema2 is ASDF)
val child = schema2.child
assertTrue(child is Bar)
assertEquals(42, child.bar)
}
Expected behavior No internal exception
Environment
- Kotlin version: 2.0.20-RC
- Library version: 1.7.1
- Kotlin platforms: JVM
- Gradle version: 8.9
- IDE version (if bug is related to the IDE): -
- Other relevant context: -
Does this still present a problem if the custom serializers are not in the companion? There might be an initialisation loop here.
Does this still present a problem if the custom serializers are not in the companion?
Nope, I also tested it and it also does not work, I get the same error message.
The fix does not work when using the interface in another type, eg in a Map:
@Serializable
public sealed interface Schema {
@KeepGeneratedSerializer
@Serializable(with = OBJECT.CustomSerializer::class)
public data class OBJECT(
val properties: Map<String, Schema>,
) : Schema {
internal object CustomSerializer : KSerializer<OBJECT> by OBJECT.generatedSerializer()
}
}
@Test
fun decodeTest() {
Json.decodeFromString(Schema.serializer(), "")
}
I created https://youtrack.jetbrains.com/issue/KT-71072/KxSerialization-KeepGeneratedSerializer-and-sealed-class-in-Map-causes-initialization-error