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

Incorrect SerialName used when using delegate serializer on a member of a sealed interface

Open FlorianDenis opened this issue 1 year ago • 3 comments

Describe the bug

I have the suspicion that the incorrect SerialName is used when using a delegate serializer on a class implementing a sealed interface. This could just be a configuration issue, but after carefully reading the doc, I cannot figure out what I am doing wrong.

To Reproduce I have JSONs of the form

{"tag": "A"}
{"tag": "B", "field1": "someString"}
{"tag": "C", "aField": "someString", "anotherField": 42}

I am trying to build a sealed class hierarchy to read/write those here, only I want each individual data of the variant to be represented by a data class (for reuse, the same fields are used across several places with different "tag" discriminators depending on other factors)

@Serializable
data class MyReusedClass(
    val aField: String,
    val anotherField: UInt
)

@Serializable
@JsonClassDiscriminator("tag")
sealed interface MySealedInterface {
    @Serializable
    @SerialName("A")
    data object A : MySealedInterface // Works as you would expect

    @Serializable
    @SerialName("B")
    data class B(val field1: String) : MySealedInterface // Works as you would expect

    @Serializable(with = C.Serializer::class) // Delegating the serializer to `value` doesn't work here, the wrong "tag" is included
    data class C(val value: MyReusedClass) : MySealedInterface {
        object Serializer : KSerializer<C> {
            private val delegateSerializer = MyReusedClass.serializer()
            override val descriptor =
                SerialDescriptor("C", delegateSerializer.descriptor) // The serialName is supposed to be "C"

            override fun serialize(encoder: Encoder, value: C) {
                encoder.encodeSerializableValue(delegateSerializer, value.value)
            }

            override fun deserialize(decoder: Decoder): C {
                return C(decoder.decodeSerializableValue(delegateSerializer))
            }
        }
    }
}

Expected behavior

When serializing MySealedInterface.C(MyReusedClass("someString", 42u)) into JSON, I get {"tag": "C", "aField": "someString", "anotherField": 42}

Actual behavior

When serializing MySealedInterface.C(MyReusedClass("someString", 42u)) into JSON, I get {"tag": "my.package.name.MyReusedClass", "aField": "someString", "anotherField": 42}

Environment

  • Kotlin version: 1.9.10
  • Library version: 1.6.0
  • Kotlin platforms: Android
  • Gradle version: 8.0
  • IDE version: Android Studio 2022.3.1 Patch 1

FlorianDenis avatar Mar 06 '24 19:03 FlorianDenis

Yes, it is a bug. It is related to the fact that serial name is written inside a beginStructure call, that happens inside encodeSerializableValue(delegateSerializer), so delegateSerializer.serialName is used instead of the "C". Probably related to #2451 and #2288

sandwwraith avatar Mar 12 '24 12:03 sandwwraith

Any way to work around it so that I can produce the JSON I am expecting (without manually duplicating the encoding for MyReusedClass) ?

FlorianDenis avatar Mar 12 '24 22:03 FlorianDenis

Perhaps you can workaround it by manually encoding value.value to JsonElement and inserting type in it. See here: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#under-the-hood-experimental

sandwwraith avatar Mar 13 '24 10:03 sandwwraith

Fixed in 1.7.0

shanshin avatar Jun 12 '24 19:06 shanshin