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

Serializer of kind ENUM cannot be serialized polymorphically with class discriminator

Open P1NG2WIN opened this issue 4 years ago • 4 comments
trafficstars

Describe the bug java.lang.IllegalArgumentException: Serializer for ChangeAlarmTypeRequest of kind ENUM cannot be serialized polymorphically with class discriminator

All subclasses of MethodExecuteCode have this problem

To Reproduce

polymorphic(MethodExecuteCode::class) {
    subclass(MethodExecuteStateTypeCode::class)
    subclass(ChangeAlarmTypeRequest::class)
    subclass(IncidentMethodCode::class)
}


interface MethodExecuteCode

@Serializable
enum class MethodExecuteStateTypeCode: MethodExecuteCode {

    @SerialName("create") CREATE,
    @SerialName("enable") ENABLE,
    @SerialName("disable") DISABLE,
    @SerialName("delete") DELETE,

}

@Serializable
enum class ChangeAlarmTypeRequest : MethodExecuteCode {
    @SerialName("alarm") START,
    @SerialName("cancel") STOP,
    @SerialName("update") UPDATE
}

@Serializable
enum class IncidentMethodCode: MethodExecuteCode {

    @SerialName("accept") ACCEPT,
    @SerialName("reject") REJECT,

}

Environment

  • Kotlin version: 1.5.0
  • Library version: 1.2.1
  • Kotlin platforms: JVM Android
  • Gradle version: 7.0.0
  • Other relevant context: Java 8

P1NG2WIN avatar May 14 '21 18:05 P1NG2WIN

As the error indicates, "ENUM cannot be serialized polymorphically with class discriminator".

The reason for this is enums are serialized as a primitive kind by default. E.g., in the case of JSON it simply tries to output the SerialName. There is no JSON object to place the class discriminator in, e.g., {"type": "fully.qualified.Name"} by default.

The good news is kotlinx.serialization is quite flexible and you can set up a custom serializer to work around this. This is why I wrote a PolymorphicEnumSerializer.

A serializer which supports registering [Enum]s as subclasses in polymorphic serialization when class discriminators are used. When class discriminators are used, an enum is not encoded as a structure which the class discriminator can be added to. An exception is thrown when initializing [Json]: " "Serializer for of kind ENUM cannot be serialized polymorphically with class discriminator." This serializer encodes the enum as a structure with a single value holding the enum value.

Use this serializer to register the enum in the serializers module, e.g.: subclass( <enum>::class, PolymorphicEnumSerializer( <enum>.serializer() )

Whathecode avatar May 18 '21 21:05 Whathecode

@Whathecode Actually, i dont want to have a class discriminator, the default enum serializer behaviour is really what i want

e.g:

@Serializable
class MethodExecute(@SerialName("code") val code: MethodExecuteCode? = null,) with ChangeAlarmTypeRequest.START must be {"code":"alarm"}

I tried to write my custom serializer based on your code by replacing buildClassSerialDescriptor with PrimitiveSerialDescriptor but I get similar primary error

P1NG2WIN avatar May 18 '21 22:05 P1NG2WIN

Actually, i dont want to have a class discriminator, the default enum serializer behaviour is really what i want

The Json encoder is configured to use a class discriminator by default for polymorphic serialization. The fact that you are getting that error message means that the Json encoder is configured as such (the alternative being array polymorphism in which case type is specified as the first element of a two-element array).

A class discriminator needs to be added in order to be able to deserialize polymorphic type (MethodExecuteCode is polymorphic), unless you are using the Json encoder and you set up a custom JsonContentPolymorphicSerializer.

Think of it this way: how does the deserializer know to initialize ChangeAlarmTypeRequest based on {"code":"alarm"}? E.g., what if there is also an alarm in IncidentMethodCode? Should it deserialize as IncidentMethodCode.ALARM or as ChangeAlarmTypeRequest.START?

The JsonContentPolymorphicSerializer allows you to based on the content (e.g. "alarm") select the serializer (e.g. IncidentMethodCode.serializer()). But, you'd only be able to implement that logic as a two-way conversion if you can uniformly determine based on content which serializer to use. Once you can do that, the type information is irrelevant. But, by default the type information is added because that's a big assumption and requires custom serialization logic based on business logic. Not adding the type also forms a maintenance risk. Now anybody adding additional MethodExecuteCode instances needs to be aware about this and update your custom serializer. You could also ask yourself, what is the use case for this base class either way?

Whathecode avatar May 19 '21 12:05 Whathecode

@Whathecode I don't need to deserialize this json. It will go to the server

P1NG2WIN avatar May 19 '21 18:05 P1NG2WIN