Is there a more elegant way of doing this? I know ...
# serialization
r
Is there a more elegant way of doing this? I know a sealed super type with polymorphic serialization would also do it, but it feels awkward if there is only one implementation.
Copy code
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class SomeEvent(
    val subject: String,
) {
    @EncodeDefault
    @Required
    private var type = "SOME_EVENT"

    init {
        require(type == "SOME_EVENT") { "'type' field of SomeEvent must be 'SOME_EVENT' but was '$type'" }
    }
}

fun main() {
    println(Json.encodeToString(SomeEvent("foo"))) // {"subject":"foo","type":"SOME_EVENT"}
    println(Json.decodeFromString<SomeEvent>("""{"subject": "foo", "type": "SOME_EVENT"}""")) // SomeEvent(subject=foo)
    println(
        try {
            Json.decodeFromString<SomeEvent>("""{"subject": "bar", "type": "OTHER_EVENT"}""")
        } catch (e: IllegalArgumentException) {
            e.message
        }
    ) // 'type' field of SomeEvent must be 'SOME_EVENT' but was 'OTHER_EVENT'
}
e
I dunno if this is more elegant, but it is more general
Copy code
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*

@Serializable
data class SomeEvent(
    val subject: String,
) {
    object TypedSerializer : KSerializer<SomeEvent> by SerializerWithExtras(serializer(), mapOf("type" to "SomeEvent"))
}

class SerializerWithExtras<T>(
    val serializer: KSerializer<T>,
    val extras: Map<String, String>,
) : KSerializer<T> {
    override val descriptor = with(serializer.descriptor) {
        require(kind is StructureKind)
        buildClassSerialDescriptor("${serialName}WithExtras") {
            for (i in 0..<elementsCount) {
                element(getElementName(i), getElementDescriptor(i), getElementAnnotations(i), isElementOptional(i))
            }
            for (key in extras.keys) element<String>(key)
        }
    }

    override fun serialize(encoder: Encoder, value: T) {
        val encoder = object : Encoder by encoder {
            override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
                check(descriptor == serializer.descriptor)
                return encoder.beginStructure(this@SerializerWithExtras.descriptor).apply {
                    for ((i, value) in extras.values.withIndex()) {
                        encodeStringElement(this@SerializerWithExtras.descriptor, serializer.descriptor.elementsCount + i, value)
                    }
                }
            }
        }
        serializer.serialize(encoder, value)
    }

    override fun deserialize(decoder: Decoder): T {
        val decoder = object : Decoder by decoder {
            override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
                check(descriptor == serializer.descriptor)
                val decoder = decoder.beginStructure(this@SerializerWithExtras.descriptor)
                return object : CompositeDecoder by decoder {
                    val extraElements = mutableSetOf<String>()

                    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
                        check(descriptor == serializer.descriptor)
                        while (true) {
                            val index = decoder.decodeElementIndex(this@SerializerWithExtras.descriptor)
                            if (index - serializer.descriptor.elementsCount !in 0..<extras.size) return index
                            val key = this@SerializerWithExtras.descriptor.getElementName(index)
                            val expectedValue = extras.getValue(key)
                            val actualValue = decodeStringElement(this@SerializerWithExtras.descriptor, index)
                            if (expectedValue != actualValue) throw SerializationException("$key: '$actualValue' != '$expectedValue'")
                            extraElements.add(key)
                        }
                    }

                    override fun endStructure(descriptor: SerialDescriptor) {
                        if (extraElements != extras.keys) throw SerializationException("Missing ${extras.keys - extraElements}")
                        decoder.endStructure(descriptor)
                    }
                }
            }
        }
        return serializer.deserialize(decoder)
    }
}

fun main() {
    println("Regular encode")
    println(Json.encodeToString(SomeEvent("hello")))
    println()

    println("Typed encode")
    println(Json.encodeToString(SomeEvent.TypedSerializer, SomeEvent("hello")))
    println()

    println("Regular decode with type")
    println(runCatching { Json.decodeFromString<SomeEvent>("""{"type":"SomeEvent","subject":"world"}""") })
    // => Encountered an unknown key 'type'
    println()

    println("Typed decode with type")
    println(Json.decodeFromString(SomeEvent.TypedSerializer, """{"type":"SomeEvent","subject":"world"}"""))
    println()

    println("Typed decode with wrong type")
    println(runCatching { Json.decodeFromString(SomeEvent.TypedSerializer, """{"type":"NotSomeEvent","subject":"world"}""") })
    // => type: 'NotSomeEvent' != 'SomeEvent'
    println()

    println("Typed decode with missing type")
    println(runCatching { Json.decodeFromString(SomeEvent.TypedSerializer, """{"subject":"world"}""") })
    // => Missing [type]
    println()
}
(also Encoder/Decoder are not stable for inheritance so this is definitely not suitable for libraries)
if you wanted to change how
SomeEvent
serializes by default, you could
Copy code
@Serializable(with = SomeEvent.TypedSerializer::class)
@KeepGeneratedSerializer
data class SomeEvent(
    val subject: String,
) {
    object TypedSerializer : KSerializer<SomeEvent> by SerializerWithExtras(generatedSerializer(), mapOf("type" to "SomeEvent"))
}