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

2.0.20 with generatedSerializer and reference to interface: SealedClassSerializer init: subclassSerializers contains null

Open hfhbd opened this issue 1 year ago • 3 comments

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: -

hfhbd avatar Aug 04 '24 12:08 hfhbd

Does this still present a problem if the custom serializers are not in the companion? There might be an initialisation loop here.

pdvrieze avatar Aug 05 '24 09:08 pdvrieze

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.

hfhbd avatar Aug 05 '24 09:08 hfhbd

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

hfhbd avatar Aug 29 '24 14:08 hfhbd