kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

coerceInputValues prevent custom enum serializer to be used

Open NoZomIBK opened this issue 2 years ago • 2 comments

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

NoZomIBK avatar May 24 '22 11:05 NoZomIBK

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.

sandwwraith avatar Jun 09 '22 18:06 sandwwraith

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

NoZomIBK avatar Jun 09 '22 18:06 NoZomIBK