Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.41k stars 620 forks source link

Internal compiler error or stack overflow when polymorphically serializing an enum that implements a sealed interface #2171

Open uliSchuster opened 1 year ago

uliSchuster commented 1 year ago

Describe the bug

I have the following sealed interface (simplified example):

sealed interface Validation { val result: Int }

...which is implemented by several enums - each one for a particular application. For example:

@Serializable(with = BoxedSerializer) // one of my attempts, see below
enum class AccountValidation(override val result: Int): Validation {
  UNKNOWN(10),
  BLOCKED(20),
  OK(30)
}

@Serializable(with = BoxedSerializer) // one of my attempts, see below
enum class PasswordValidation(override val result: Int): Validation {
  SHORT(10),
  WEAK(20),
  OK(30)
}

I want to use this enum polymorphically in serializable API types, like so:

@Serializable
data class ValidationResult(
  val userName: String,
  val validation: Validation
)

Out-of-the-box, this setup does not work, because enums are serialized as primitive types, without a type field for polymorphic serialization. I tried several other attempts:

To Reproduce

Surrogate serialization in a generic wrapper class also fails with an internal compiler error:

@Serializable
@SerialName("Validation")
data class ValidationBox<T : Validation>(val code: T)

class BoxedSerializer<T : Validation>(private val validationSerializer: KSerializer<T>) : KSerializer<T> {
    private val boxSerializer = ValidationBox.serializer(validationSerializer)
    override val descriptor: SerialDescriptor = boxSerializer.descriptor

    override fun serialize(encoder: Encoder, value: T) {
        val boxed = ValidationBox(value)
        encoder.encodeSerializableValue(boxSerializer, boxed)
    }

    override fun deserialize(decoder: Decoder): T {
        val boxed: ValidationBox<T> = decoder.decodeSerializableValue(boxSerializer)
        return boxed.code
    }
}

@Test
fun `polymorphically serialize and deserialize`() {
    val validation: Validation = AccountValidation.BLOCKED
    val validationJson = Json.encodeToString(validation)
    val validationDeserialized = Json.decodeFromString<Validation>(validationJson)
    assertEquals(validation, validationDeserialized)
}

Expected behavior

What I would like to get as output (JSON example):

{
  "userName": "myUserName",
  "validation": {"PasswordValidation": "WEAK"}
}

or (closer to the standard)

{
  "userName": "myUserName",
  "validation": {
      "type": "PasswordValidation",
      "value": "WEAK"
    }
}

How would a semi-custom or (if necessary) full-custom serializer look like?

Thanks for your help!

Environment

sandwwraith commented 1 year ago

The BoxedSerializer leads to an internal error because it expects that validationSerializer property would be provided. Because enums do not have any type parameters, there is nothing to insert. We should add proper diagnostic there

Regardless stack overflow with surrogate serializer — can you please show an example?

I don't think that now it is possible to encode enums polymorphically (also #2164) unless you write a fully custom serializer for sealed interface Validation. Alternatively, you can try to change all enums into sealed classes, and enum entries — to objects, as objects mix well into sealed hierarchies.