Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
hfhbd opened this issue Aug 4, 2024 · 3 comments

Comments

@hfhbd
Copy link
Contributor

hfhbd commented Aug 4, 2024

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: -
@pdvrieze
Copy link
Contributor

pdvrieze commented Aug 5, 2024

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

@hfhbd
Copy link
Contributor Author

hfhbd commented Aug 5, 2024

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.

shanshin added a commit to JetBrains/kotlin that referenced this issue Aug 6, 2024
…lizer feature with sealed classes

When the heir of a sealed class contains this sealed class as a property, cyclic initialization occurs due to the fact that the SealedClassSerializer in the constructor accesses all passed subclass serializers.

Initialization order:
Parent Sealed class serializer
\/
Create instance of SealedClassSerializer for Parent Sealed
\/
Access subclass serializer in constructor of SealedClassSerializer
\/
Call ChildClass.Compaion.generatedSerializer()
\/
Init ChildClass
\/
cache child serializers of ChildClass
\/
Create instance of SealedClassSerializer for Parent Sealed ---> /\

Fixes Kotlin/kotlinx.serialization#2759
shanshin added a commit to JetBrains/kotlin that referenced this issue Aug 6, 2024
…lizer feature with sealed classes

When the heir of a sealed class contains this sealed class as a property, cyclic initialization occurs due to the fact that the SealedClassSerializer in the constructor accesses all passed subclass serializers.

Initialization order:
Parent Sealed class serializer
\/
Create instance of SealedClassSerializer for Parent Sealed
\/
Access subclass serializer in constructor of SealedClassSerializer
\/
Call ChildClass.Compaion.generatedSerializer()
\/
Init ChildClass
\/
cache child serializers of ChildClass
\/
Create instance of SealedClassSerializer for Parent Sealed ---> /\

Fixes Kotlin/kotlinx.serialization#2759
@hfhbd
Copy link
Contributor Author

hfhbd commented Aug 29, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants