Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating serial descriptor when serializing to heterogeneous lists #2899

Open
odzhychko opened this issue Jan 8, 2025 · 4 comments
Open

Comments

@odzhychko
Copy link

odzhychko commented Jan 8, 2025

What is your use-case and why do you need this feature?

I want to serialize a class as a JSON array. The elements can have different types.

For that, I created a custom serializer. When building the corresponding SerialDescriptor I need to use the internal buildSerialDescriptor builder because the public buildClassSerialDescriptor does not cover this use-case.

The following shows a simplified example:

package dev.oleks

import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeCollection
import kotlinx.serialization.json.Json

fun main() {
    val encoded = Json.encodeToString(ListOfThreeElements(1, "aValue1", SomeClass("aValue2")))
    println(encoded) // [1,"aValue1",{"someAttribute":"aValue2"}]
    val decoded: ListOfThreeElements<Int, String, SomeClass> = Json.decodeFromString(encoded)
    println(decoded) // ListOfThreeElements(value1=1, value2=aValue1, value3=SomeClass(someAttribute=aValue2))
}

@Serializable
data class SomeClass(val someAttribute: String)

@Serializable(with = ListOfThreeElementsSerializer::class)
data class ListOfThreeElements<T1 : Any, T2 : Any, T3 : Any>(
    val value1: T1,
    val value2: T2,
    val value3: T3,
)

class ListOfThreeElementsSerializer<T1 : Any, T2 : Any, T3 : Any>(
    private val t1Serializer: KSerializer<T1>,
    private val t2Serializer: KSerializer<T2>,
    private val t3Serializer: KSerializer<T3>,
) : KSerializer<ListOfThreeElements<T1, T2, T3>> {
    override fun deserialize(decoder: Decoder): ListOfThreeElements<T1, T2, T3> {
        var value1: T1? = null
        var value2: T2? = null
        var value3: T3? = null
        decoder.decodeStructure(descriptor) {
            while (true) {
                when (val index = decodeElementIndex(descriptor)) {
                    0 -> value1 = decodeSerializableElement(descriptor, index, t1Serializer)
                    1 -> value2 = decodeSerializableElement(descriptor, index, t2Serializer)
                    2 -> value3 = decodeSerializableElement(descriptor, index, t3Serializer)
                    CompositeDecoder.DECODE_DONE -> break // Input is over
                    else -> error("Unexpected index: $index")
                }
            }
        }
        require(value1 != null && value2 != null && value3 != null)
        return ListOfThreeElements(value1, value2, value3)
    }

    override val descriptor: SerialDescriptor = run {
        val typeParameters = arrayOf(t1Serializer.descriptor, t2Serializer.descriptor, t3Serializer.descriptor)
        @OptIn(InternalSerializationApi::class) (buildSerialDescriptor (
        "dev.oleks.ListOfThreeElements", StructureKind.LIST, *typeParameters
    ) {
        element("0", t1Serializer.descriptor)
        element("1", t1Serializer.descriptor)
        element("2", t1Serializer.descriptor)
    })
    }

    override fun serialize(encoder: Encoder, value: ListOfThreeElements<T1, T2, T3>) {
        encoder.encodeCollection(descriptor, 3) {
            encodeSerializableElement(t1Serializer.descriptor, 0, t1Serializer, value.value1)
            encodeSerializableElement(t2Serializer.descriptor, 1, t2Serializer, value.value2)
            encodeSerializableElement(t3Serializer.descriptor, 2, t3Serializer, value.value3)
        }
    }
}

Describe the solution you'd like

  • A function like listSerialDescriptor but for heterogeneous lists,
  • making non-internalbuildSerialDescriptor or
  • a buildListSerialDescriptor builder similar to buildClassSerialDescriptor and buildSerialDescriptor.
@sandwwraith
Copy link
Member

Hm, why do you need exactly LIST and it can't be just some kind of Tuple3 class?

@odzhychko
Copy link
Author

Hm, why do you need exactly LIST and it can't be just some kind of Tuple3 class?

I'm not sure, I understood your question correctly.

I want to serialize a ListOfThreeElements to a JSON array (e.g., [1,"aValue1",{"someAttribute":"aValue2"}]) instead of a JSON object (e.g., {"value1":1,"value2":"aValue1", "value3" : { "someAttribute" : "aValue2"}} because that is the data format of the API we decided on in our project. I guess initially, we wanted to achieve smaller responses by avoiding keys.

To serialize/deserialize a JSON array, the SerialDescriptor.kind needs to be set to StructureKind.LIST. Is this a wrong understanding of how Kotlin serialization works? Can an object be serialized to a JSON array when the kind is set to StructureKind.CLASS?

@odzhychko
Copy link
Author

I just noticed that I made a mistake in the example.
I changed the following in the example just now:

58,59c60,61
<         @OptIn(InternalSerializationApi::class) (buildClassSerialDescriptor (
<         "dev.oleks.ListOfThreeElements", *typeParameters
---
>         @OptIn(InternalSerializationApi::class) (buildSerialDescriptor (
>         "dev.oleks.ListOfThreeElements", StructureKind.LIST, *typeParameters

@sandwwraith
Copy link
Member

Thanks for the clarifications. Yes, if you want [ ] brackets in the output, then using StructureKind.LIST is the correct approach. We currently do not support heterogeneous lists natively, so opting-in into internal API is the only way. We'll add this use-case to our list when we'll be designing buildSerialDescriptor for public use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants