Richard Schielek
10/01/2025, 10:30 PMimport 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'
}
ephemient
10/01/2025, 11:23 PMimport 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)ephemient
10/01/2025, 11:25 PMephemient
10/02/2025, 12:02 AMSomeEvent
serializes by default, you could
@Serializable(with = SomeEvent.TypedSerializer::class)
@KeepGeneratedSerializer
data class SomeEvent(
val subject: String,
) {
object TypedSerializer : KSerializer<SomeEvent> by SerializerWithExtras(generatedSerializer(), mapOf("type" to "SomeEvent"))
}