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

IndexOutOfBoundsException by getElementDescriptor with @Serializer

Open Ktlo opened this issue 5 years ago • 5 comments

Describe the bug

When using @Serializer, the following exception is thrown when trying to get an element descriptor.

java.lang.IndexOutOfBoundsException: Example descriptor has only 1 elements, index: 0

To Reproduce

data class Example(
    val value: String
)

@Serializer(Example::class)
object ExampleSerializer

fun main() {
    println(ExampleSerializer.descriptor.getElementName(0)) // Everything is fine here
    println(ExampleSerializer.descriptor.getElementDescriptor(0)) // IndexOutOfBoundsException
}

Expected behavior

Method SerialDescriptor::getElementDescriptor should return a SerialDescriptor.

Environment

  • Kotlin version: 1.3.72
  • Library version: 0.20.0
  • Kotlin platforms: JVM
  • Gradle version: 6.4.1

Ktlo avatar May 18 '20 21:05 Ktlo

@sandwwraith this is a bug in the compiler plugin:

@Serializable
data class Example(val value: String)

@Serializer(Example::class)
object ExampleSerializer

In this example, Example.serializer().descriptor and ExampleSerializer.descriptor() have different structure. Please ensure that such descriptors are generated by the same code

qwwdfsad avatar May 19 '20 08:05 qwwdfsad

@qwwdfsad Your case is not the same as in the original issue. When Example annotated as Serializable, the serializer is also generated inside. It should be like

@Serializable(ExampleSerializer::class)
data class Example(val value: String)

@Serializer(Example::class)
object ExampleSerializer

sandwwraith avatar May 19 '20 13:05 sandwwraith

I know, I've deliberately exposed it.

Auto-generated serializer in nested companion (@Serializable data class ...) and auto-generated external serializer (@Serializer(Example::class)) have different SerialDescriptor structures.

Two bugs here:

  1. These descriptors are generated by different code paths (it should be the same code path)
  2. The latter codegen has the bug

qwwdfsad avatar May 19 '20 14:05 qwwdfsad

While we all waiting for this to be fixed I've just found a workaround that worked for me. I wanted to delegate to auto-generated serializer and just add some logic like this:

@Serializable
data class MyDTO(
    val someField: String
)

object MyDTOCustomSerializer: KSerializer<MyDTO> by MyDTO.serializer() {
    override fun deserialize(decoder: Decoder): MyDTO {
        // do stuff
        // delegate deserialization to generated serializer
        return MyDTO.serializer().deserialize(decoder)
    }
}

Though I didn't find a way to make framework use my MyDTOCustomSerializer, if I put it in @Serializable annotation it will just create circular reference. My workaround is using type alias:

@Serializable
data class MyDTOHidden(
    val someField: String
)
typealias MyDTO = @Serializable(MyDTOCustomSerializer::class) MyDTOHidden

object MyDTOCustomSerializer: KSerializer<MyDTO> by MyDTOHidden.serializer() {
    override fun deserialize(decoder: Decoder): MyDTO {
        // do stuff
        // delegate deserialization to original serializer
        return MyDTOHidden.serializer().deserialize(decoder)
    }
}

Am I overthinking it?

AngryGami avatar Aug 11 '23 14:08 AngryGami

@AngryGami That's an adequate solution. You can also vote for #1169.

sandwwraith avatar Aug 14 '23 11:08 sandwwraith