-
-
Save arberg/e20db05e018c61f37f1d274a254657c3 to your computer and use it in GitHub Desktop.
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) | |
} |
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.
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. 🙂
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?
Yes, that's what I started from. Serializer itself is preserved, but reflective logic inside it fails.
When using this on Android, keep in mind that it's not compatible with R8 without additional setup (due to using reflection):
getField()
fails withNoSuchFieldException: One