Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.42k stars 620 forks source link

`encodeToJsonElement` crashes with generic `ContextualSerializer` #2535

Open httpdispatch opened 11 months ago

httpdispatch commented 11 months ago

Describe the bug When i try to encode generic data to the JSON element using ContextualSerializer, the following crash occurs

Empty list doesn't contain element at index 0.
java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 0.
    at kotlin.collections.EmptyList.get(Collections.kt:37)
    at kotlin.collections.EmptyList.get(Collections.kt:25)
    at GenericContextualTest$createJson$1$1$1.invoke(GenericContextualTest.kt:55)
    at GenericContextualTest$createJson$1$1$1.invoke(GenericContextualTest.kt:55)
    at kotlinx.serialization.modules.ContextualProvider$WithTypeArguments.invoke(SerializersModule.kt:231)
    at kotlinx.serialization.modules.SerialModuleImpl.getContextual(SerializersModule.kt:172)
    at kotlinx.serialization.modules.SerializersModule.getContextual$default(SerializersModule.kt:48)
    at kotlinx.serialization.descriptors.ContextAwareKt.getContextualDescriptor(ContextAware.kt:61)
    at kotlinx.serialization.json.internal.WriteModeKt.carrierDescriptor(WriteMode.kt:49)
    at kotlinx.serialization.json.internal.AbstractJsonTreeEncoder.encodeSerializableValue(TreeJsonEncoder.kt:79)
    at kotlinx.serialization.json.internal.TreeJsonEncoderKt.writeJson(TreeJsonEncoder.kt:21)
    at kotlinx.serialization.json.Json.encodeToJsonElement(Json.kt:117)
    at GenericContextualTest.should serialize contextual to JsonElement(GenericContextualTest.kt:37)

To Reproduce

import kotlinx.serialization.ContextualSerializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.modules.SerializersModule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class GenericContextualTest {

    @Test
    fun `should serialize contextual to string`() {
        val json = createJson()
        val serializer = createContextualSerializer(
            typeArgumentSerializer = String.serializer(),
        )

        val actualResult = json.encodeToString(
            serializer,
            Box("test")
        )

        assertThat(actualResult).isEqualTo("\"test\"")
    }

    @Test
    fun `should deserialize contextual from string`() {
        val json = createJson()
        val serializer = createContextualSerializer(
            typeArgumentSerializer = String.serializer(),
        )

        val actualResult = json.decodeFromString(
            serializer,
            "\"test\""
        )

        assertThat(actualResult).isEqualTo(Box("test"))
    }

    @Test
    fun `should serialize contextual to JsonElement`() {
        val json = createJson()
        val serializer = createContextualSerializer(
            typeArgumentSerializer = String.serializer(),
        )

        val actualResult = json.encodeToJsonElement(
            serializer,
            Box("test")
        )

        assertThat(actualResult).isEqualTo(JsonPrimitive("test"))
    }

    @Test
    fun `should deserialize contextual from JsonElement`() {
        val json = createJson()
        val serializer = createContextualSerializer(
            typeArgumentSerializer = String.serializer(),
        )

        val actualResult = json.decodeFromJsonElement(
            serializer,
            JsonPrimitive("test")
        )

        assertThat(actualResult).isEqualTo(Box("test"))
    }

    fun <T> createContextualSerializer(
        typeArgumentSerializer: KSerializer<T>,
    ): KSerializer<Box<T>> = ContextualSerializer(
        serializableClass = Box::class,
        fallbackSerializer = null,
        typeArgumentsSerializers = arrayOf(typeArgumentSerializer),
    ) as KSerializer<Box<T>>

    private fun createJson() = Json {
        serializersModule = SerializersModule {
            contextual(Box::class) { args -> BoxSerializer(args[0]) }
        }
    }

    data class Box<T>(val contents: T)

    class BoxSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Box<T>> {
        override val descriptor: SerialDescriptor = dataSerializer.descriptor
        override fun serialize(encoder: Encoder, value: Box<T>) =
            dataSerializer.serialize(encoder, value.contents)

        override fun deserialize(decoder: Decoder) = Box(dataSerializer.deserialize(decoder))
    }
}

Expected behavior

Serialization should be performed properly without crash. Deserialization works as expected

Environment

sandwwraith commented 11 months ago

The problem is in getContextualDescriptor function (https://github.com/Kotlin/kotlinx.serialization/blob/45976024789fce101b650f3fe5546dd30362658a/core/commonMain/src/kotlinx/serialization/descriptors/ContextAware.kt#L60) that doesn't pass required parameters to module.getContextual. It looks like the ContextAware descriptor should store type parameters serializers, too.

httpdispatch commented 11 months ago

@sandwwraith are there any workarounds till it is fixed? The only one I've found is too use json.serializersModule.serializer() method, but it requires json reference.

sandwwraith commented 11 months ago

@httpdispatch If you know concrete type of Box type argument in advance you can replace args -> BoxSerializer(args[0]) with e.g. BoxSerializer(String.serializer())

httpdispatch commented 11 months ago

@sandwwraith unfortunately I don't know generic type in advance. Also my real contextual serializer has additional dependencies so can't create it on demand in any place. That is why I tried to use ContextualSerializer which fits well in similar cases.