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