kotlinx.serialization
kotlinx.serialization copied to clipboard
kotlinx.serialization.SerializationException: Enum class Country does not contain element with name 'MISSING' at path $[2]
Describe the bug I made several attempts, but I failed to deserialize the JSON as expected. Every time, I encountered an exception error. I had to write my own serializer to solve the issue with the serialization of List<Enum<*>> or other enum cases...
To Reproduce
class KotlinxTests {
enum class Country { USA, OTHER }
@Test
fun testSerializer() {
val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
explicitNulls = false
}
val countriesActual: List<Country?> = json.decodeFromString("""["USA", null, "MISSING"]""")
val countriesExpect: List<Country?> = listOf(USA, null, null)
require(countriesActual == countriesExpect)
}
}
My Solution If anyone is facing a similar issue, they can temporarily use my this solution:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
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
/**
* A generic serializer for enum classes using Kotlin Serialization with caches.
*
* This serializer handles the serialization and deserialization of enum values as strings,
* using either the `serialName` (if available) or the regular `name` of the enum.
*
* @param T The enum type to serialize.
*/
inline fun <reified T : Enum<T>> enumSerializer(): KSerializer<T?> = object : KSerializer<T?> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("EnumSerializer", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: T?) {
(value?.serialName ?: value?.name)?.let { encoder.encodeString(it) }
}
override fun deserialize(decoder: Decoder): T? {
val decodeString = decoder.decodeString()
return decodeString.enumBySerialName<T>() as T?
?: decodeString.enumByName<T>() as T?
?: run {
println(
"""
|enumSerializer
|Unknown enum value found: "$decodeString" in ${T::class.simpleName}
""".trimMargin()
)
null
}
}
}
/**
* A utility object that provides caching for enum name and serialized name lookups.
*
* This object maintains three caches:*
* - `serialNameByEnum`: Maps enum instances to their serialized names (as defined by the `@SerialName` annotation).
* - `enumByEnumName`: Maps enum names to their corresponding enum instances.
* - `enumBySerialName`: Maps serialized names to their corresponding enum instances.
*
* The caches are populated lazily, meaning that the mappings are generated only when a particular enum class is accessed for the first time.
*/
object Caches {
private val serialNameByEnum: MutableMap<Class<*>, Map<Enum<*>, String>> = mutableMapOf()
private val enumByEnumName: MutableMap<Class<*>, Map<String, Enum<*>>> = mutableMapOf()
private val enumBySerialName: MutableMap<Class<*>, Map<String, Enum<*>>> = mutableMapOf()
private fun <T : Enum<T>> makeCache(declaringClass: Class<T>) {
val mapNames = declaringClass.enumConstants!!
val pairs: List<Pair<T, String>> = mapNames
.mapNotNull { constant ->
val serialName = constant
.declaringJavaClass
.getField(constant.name)
.getAnnotation(SerialName::class.java)?.value
serialName?.let { constant to it }
}
serialNameByEnum[declaringClass] = pairs.toMap()
enumByEnumName[declaringClass] = mapNames.associateBy { it.name }
enumBySerialName[declaringClass] = pairs.associate { it.second to it.first }
}
fun <T : Enum<T>> serialNameByEnum(enum: Enum<T>): String? {
val declaringClass: Class<T> = enum.declaringJavaClass
serialNameByEnum[declaringClass] ?: makeCache(declaringClass)
return serialNameByEnum[declaringClass]!![enum]
}
fun <T : Enum<T>> enumByName(declaringClass: Class<T>, serialName: String): Enum<*>? {
enumByEnumName[declaringClass] ?: makeCache(declaringClass)
return enumByEnumName[declaringClass]!![serialName]
}
fun <T : Enum<T>> enumBySerialName(declaringClass: Class<T>, serialName: String): Enum<*>? {
enumBySerialName[declaringClass] ?: makeCache(declaringClass)
return enumBySerialName[declaringClass]!![serialName]
}
}
/**
* Returns the serialized name of the enum instance, as defined by the `@SerialName` annotation.
*
* @returnThe serialized name of the enum, or `null` if no `@SerialName` annotation is present.
*/
val <T : Enum<T>> Enum<T>.serialName: String?
get() = Caches.serialNameByEnum(this)
/**
* Attempts to findan enum instance of the reified type [T] by its simple name.
*
* @return The enum instance corresponding to the given name, or `null` if not found.
*/
inline fun <reified T : Enum<T>> String.enumByName(): Enum<*>? =
Caches.enumByName(T::class.java, this)
/**
* Attempts to find an enum instance of the reified type [T] by its serialized name.
*
* @return The enum instance corresponding to the given serialized name, or `null` if not found.
*/
inline fun <reified T : Enum<T>> String.enumBySerialName(): Enum<*>? =
Caches.enumBySerialName(T::class.java, this)
Using My custom enumSerializer in our test.
class KotlinxTests {
object EnumSerializer : KSerializer<Country?> by enumSerializer()
@Serializable(with = EnumSerializer::class)
enum class Country { USA, OTHER }
@Test
fun testSerializer() {
val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
explicitNulls = false
}
val countriesActual: List<Country?> = json.decodeFromString("""["USA", null, "MISSING"]""")
val countriesExpect: List<Country?> = listOf(USA, null, null)
require(countriesActual == countriesExpect)
}
}
Expected behavior
listOf(USA, null, null)
Environment
- Kotlin version: 2.0.0
- Library version: 1.7.0
- Kotlin platforms: JVM
- Gradle version: 8.5
- IDE version: IntelliJ IDEA 2024.1.4 (241.18034.62 build)
- Microsoft Windows 11 Pro
- JRE 21