Ulrich Schuster
Ulrich Schuster

Reputation: 1906

How to polymorphically serialize a Kotlin enum that implements a sealed interface?

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
)

I am using Kotlin 1.7.21 with kotlin serialization 1.4.1 on the JVM. 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:

@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)
}
   

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!

Upvotes: 4

Views: 944

Answers (2)

navid
navid

Reputation: 1398

Given the situation we can serialize enums as Map. I wrote a base class:

abstract class EnumAsMapSerializer<T>(sName: String) : KSerializer<T> {
    private val mapSerializer: KSerializer<Map<String, String>> = serializer()

    @OptIn(ExperimentalSerializationApi::class)
    override val descriptor = SerialDescriptor(sName, mapSerializer.descriptor)

    abstract fun getTypeOf(value: T): String

    abstract fun enumFrom(type: String, value: String): T

    override fun serialize(encoder: Encoder, value: T) {
        encoder.encodeSerializableValue(
            mapSerializer, mapOf(
                "type" to getTypeOf(value),
                "value" to (value as Enum<*>).name,
            )
        )
    }

    override fun deserialize(decoder: Decoder): T {
        val map = decoder.decodeSerializableValue(mapSerializer)
        return enumFrom(map["type"]!!, map["value"]!!)
    }
}

In your case it can be extended and used like this:

class ValidationSerialization : EnumAsMapSerializer<Validation>("Validation") {
    override fun getTypeOf(value: Validation): String = when (value) {
        is AccountValidation -> getTypeSerialName<AccountValidation>()
        is PasswordValidation -> getTypeSerialName<PasswordValidation>()
    }

    override fun enumFrom(type: String, value: String): Validation = when (type) {
        getTypeSerialName<AccountValidation>() -> AccountValidation.valueOf(value)
        getTypeSerialName<PasswordValidation>() -> PasswordValidation.valueOf(value)
        else -> throw IllegalArgumentException("Undefined serialization for type: $type")
    }
}

@OptIn(ExperimentalSerializationApi::class)
private inline fun <reified T> getTypeSerialName() = serializer<T>().descriptor.serialName

@Serializable(with = ValidationSerialization::class)
sealed interface Validation {...}

which serializes the ValidationResult's instance as:

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

You can also set SerialName to change the type value:

@SerialName("PasswordValidation")
enum class PasswordValidation {...}

Then you get:

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

Upvotes: 0

trom
trom

Reputation: 61

I had the same problem, so I used custom serialization:

@JsonSerialize(using = EnumTypeSerializer::class)
@JsonDeserialize(using = EnumTypeDeserializer::class)
sealed class { enum1, enum2 }


object Serializer : JsonSerializer<EnumType>() {
override fun serialize(value: EnumType, jsonGenerator: JsonGenerator, serializers: SerializerProvider) {
    val name = when(value) {
        is EnumType.FIRST-> value.name
        is EnumType.SECOND-> value.name
    }
    jsonGenerator.writeString(name)
}

object NotificationGroupTypeDeserializer : JsonDeserializer<EnumType>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext?): EnumType{
    return EnumType.fromString(parser.valueAsString)
}

}

Upvotes: 0

Related Questions