kotlinx.serialization
kotlinx.serialization copied to clipboard
coerceInputValues prevent custom enum serializer to be used
Describe the bug
Having Json configured with coerceInputValues = true
results in Enums not being deserialized if they have a custom serializer. If a value is predefined in the data object, it uses the predefined value, if it has to be passed as a constructor parameter, deserialization fails because it is "missing"
Set a breakpoint in the first line of BarSerialiizer.deserialize, but debugger did not go there, no further error message etc.
To Reproduce
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.SerialKind.ENUM
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
@Serializable
enum class Foo {
A,
B
}
@Serializable(with = BarSerializer::class)
enum class Bar {
C,
D
}
object BarSerializer : KSerializer<Bar> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("BarSerializer", ENUM)
override fun deserialize(decoder: Decoder): Bar {
val stringValue = decoder.decodeString()
return try {
Bar.valueOf(stringValue)
}
catch (e: IllegalArgumentException) {
Bar.C
}
}
override fun serialize(encoder: Encoder, value: Bar) {
encoder.encodeString(value.name)
}
}
@Serializable
class SomeClass {
var foo: Foo = Foo.A
var bar: Bar = Bar.C
}
@Serializable
class OtherClass(var foo:Foo,var bar:Bar)
class EnumBugTest {
@Test
fun serializerNotUsed() {
// given
val json = """
{
"foo":"B",
"bar":"D"
}
""".trimIndent()
// when
val obj: SomeClass = Json {
coerceInputValues = true
}.decodeFromString(json)
// then
assertEquals(Foo.B, obj.foo)
assertEquals(Bar.D, obj.bar) // fails because it uses the default value Bar.C in SomeClass
}
@Test
fun otherClassTest(){
// given
val json = """
{
"foo":"B",
"bar":"D"
}
""".trimIndent()
// when
val obj: OtherClass = Json {
coerceInputValues = true
}.decodeFromString(json) // fails because field "bar" is "missing"
// then
assertEquals(Foo.B, obj.foo)
assertEquals(Bar.D, obj.bar)
}
}
Expected behavior The defined serializer should be used
Environment
- Kotlin version:1.6.21
- Library version: 1.3.2
- Kotlin platforms: JVM
- Gradle version: 7.1.1
This is kinda working as designed. The point of coerceInputValues
is that it does not invoke the serializer at all. It only queries descriptor to determine if the value in input is valid or should be coerced. You could've added all additional values to the descriptor, but it does not work when the set of values is not known in advance (your case with try-catch). So, if you want to customize logic somehow, you need to disable coercing.
However, I don't get why you need this serializer in the first place. It you remove it, coerceInputValues
will automatically replace incorrect values with default (Bar.C
in your example) — the very same behavior.
My example may be a bit to short. Maybe this bit makes my intention a bit clearer:
data class Data(val type:Type = BEST)
enum class Type{
BEST,
OKAY,
UNKNOWN
}
this json:
{
"type": "BAD"
}
should result in Data(UNKNOWN)
instead of Data(BEST)
. But I still need for all the other serialization to have coerceInputValues = true