Skip to content

Instantly share code, notes, and snippets.

@arberg
Last active June 23, 2023 00:44
Show Gist options
  • Save arberg/e20db05e018c61f37f1d274a254657c3 to your computer and use it in GitHub Desktop.
Save arberg/e20db05e018c61f37f1d274a254657c3 to your computer and use it in GitHub Desktop.
Kotlin Enum Serializer which ignores unknown values (kotlinx)
import android.annotation.SuppressLint
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import org.junit.Test
import strikt.api.*
import strikt.assertions.*
abstract class EnumIgnoreUnknownSerializer<T : Enum<T>>(values: Array<out T>, private val defaultValue: T) : KSerializer<T> {
// Alternative to taking values in param, take clazz: Class<T>
// - private val values = clazz.enumConstants
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(values.first()::class.qualifiedName!!, PrimitiveKind.STRING)
// Build maps for faster parsing, used @SerialName annotation if present, fall back to name
private val lookup = values.associateBy({ it }, { it.serialName })
private val revLookup = values.associateBy { it.serialName }
private val Enum<T>.serialName: String
get() = this::class.java.getField(this.name).getAnnotation(SerialName::class.java)?.value ?: name
override fun serialize(encoder: Encoder, value: T) {
encoder.encodeString(lookup.getValue(value))
}
override fun deserialize(decoder: Decoder): T {
// only run 'decoder.decodeString()' once
return revLookup[decoder.decodeString()] ?: defaultValue // map.getOrDefault is not available < API-24
}
}
// ---- Usage example, put this in for instance TestEnum.kt
@Serializable(with = TestEnumSerializer::class)
enum class TestEnum {
One,
@SerialName("TWO_TWO")
Two,
Unknown
}
object TestEnumSerializer : EnumIgnoreUnknownSerializer<TestEnum>(TestEnum.values(), TestEnum.Unknown)
// ---- A test using Strikt framework
class EnumIgnoreUnknownSerializerTest {
// get your Json instance
val ksonServer: Json = Json
@Test
fun encodeAndDecode_WithoutSerialName() {
expectThat(encode(TestEnum.One)).isEqualTo(""""One"""")
expectThat(decode(""""One"""")).isEqualTo(TestEnum.One)
}
@Test
fun encodeAndDecode_WithSerialName() {
expectThat(encode(TestEnum.Two)).isEqualTo(""""TWO_TWO"""")
expectThat(decode(""""TWO_TWO"""")).isEqualTo(TestEnum.Two)
}
@Test
fun decodeUnknown() {
expectThat(decode(""""FooBar"""")).isEqualTo(TestEnum.Unknown)
expectThat(decode(""""Unknown"""")).isEqualTo(TestEnum.Unknown)
}
private fun encode(value: TestEnum): String = ksonServer.encodeToString(TestEnum.serializer(), value)
private fun decode(string: String) = ksonServer.decodeFromString(TestEnum.serializer(), string)
}
@gmk57
Copy link

gmk57 commented Jun 23, 2023

Yes, that's what I started from. Serializer itself is preserved, but reflective logic inside it fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment