I have an enum that I'd like to deserialize from JSON using kotlinx.serialization while ignoring unknown values. This is the enum
@Serializable
enum class OperatingMode {
Off, On, Auto
}
What I mean by ignoring unknowns is if I have a mode or modes in a JSON object which are not in that enum, they should be treated as absent:
{"foo":"bar","mode":"Banana"}
// same as
{"foo":"bar"}
{"modes":["Off","On","Banana"]}
// same as
{"modes":["Off","On"]}
I got this to work by writing custom serializers, but it seems quite verbose for such a simple task
internal object OperatingModeSafeSerializer : KSerializer<OperatingMode?> {
override val descriptor = PrimitiveSerialDescriptor("OperatingMode", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: OperatingMode?) {
// safe because @Serializable skips null fields
encoder.encodeString(value!!.name)
}
override fun deserialize(decoder: Decoder): OperatingMode? {
val string = decoder.decodeString()
return try {
OperatingMode.valueOf(string)
} catch (_: Exception) {
null
}
}
}
internal object OperatingModeSafeListSerializer: KSerializer<List<OperatingMode>> {
private val delegateSerializer = ListSerializer(OperatingModeSafeSerializer)
override val descriptor = delegateSerializer.descriptor
override fun deserialize(decoder: Decoder): List<OperatingMode> {
return decoder.decodeSerializableValue(delegateSerializer).filterNotNull()
}
override fun serialize(encoder: Encoder, value: List<OperatingMode>) {
encoder.encodeSerializableValue(delegateSerializer, value)
}
}
Then in each object which is deserializing OperatingMode
I can add
@Serializable(with = OperatingModeSafeSerializer::class) // or
@Serializable(with = OperatingModeSafeListSerializer::class)
to ignore unknowns.
@Serializable(with = ...)
needs a compile-time constant.OperatingMode
will ignore unknowns.I found a way to refactor the approach in my question to avoid most of the code duplication.
open class SafeSerializer<T>(
private val serializer: KSerializer<T>
): KSerializer<T?> {
override val descriptor = serializer.descriptor
// safe because @Serializable skips null fields
override fun serialize(encoder: Encoder, value: T?) = encoder.encodeSerializableValue(serializer, value!!)
override fun deserialize(decoder: Decoder): T? = try {
decoder.decodeSerializableValue(serializer)
} catch (_: Exception) {
null
}
}
open class NonNullListSerializer<T>(
serializer: KSerializer<T?>
): KSerializer<List<T>> {
private val delegateSerializer = ListSerializer(serializer)
override val descriptor = delegateSerializer.descriptor
override fun serialize(encoder: Encoder, value: List<T>) = encoder.encodeSerializableValue(delegateSerializer, value)
override fun deserialize(decoder: Decoder): List<T> = decoder.decodeSerializableValue(delegateSerializer).filterNotNull()
}
Then for each enum you want to deserialize in this way, you simply declare
object OperatingModeSafeSerializer: SafeSerializer<OperatingMode>(OperatingMode.serializer())
object OperatingModeSafeListSerializer: NonNullListSerializer<OperatingMode>(OperatingModeSafeSerializer)
N.B. this probably only works with basic types like enums. If you try to use SafeSerializer
with a complex type, the decoder might throw an exception in the middle of parsing a structure such as a JSON array or object. Catching that error will leave the decoder in an invalid state. One option would be to change deserialize
to first decode a generic JSON element without catching errors, then decode that to T
while catching, but that's at the cost of tightly coupling the serializer to JSON.
If kotlinx.serialization had less greedy parsers that tried to fully consume elements before throwing, this would be a non-issue.