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 19, 2023

When using this on Android, keep in mind that it's not compatible with R8 without additional setup (due to using reflection): getField() fails with NoSuchFieldException: One

@arberg
Copy link
Author

arberg commented Jun 20, 2023

I'm not using proguard and as far as I can see R8 is their proguard replacement for shrinking code. I don't use it. However if that is correct, then it is correct that you would have to avoid obfuscating all the serialized enums, because otherwise it obviously cannot find the correct names in the code. Though I wonder if that is really different compared to @serializable. I suspect they also avoid obfuscating all @serializable class.

@gmk57
Copy link

gmk57 commented Jun 20, 2023

For classes or enums marked as @Serializable, generated serialiser must be kept, and bundled proguard rules take care of that. Everything else can be obfuscated, because there is no reflection involved.

There is nothing wrong with your solution, it's just the caveat that needs to be taken into account. 🙂

@arberg
Copy link
Author

arberg commented Jun 21, 2023

Yes indeed, its a good point that the enum must be marked to be kept. I trust that you have already tested that marking the enum as @Serializable(with = TestEnumSerializer::class) as I did in the above example won't be enough?

@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