Ulrich Schuster
Ulrich Schuster

Reputation: 1916

A Kotlin Serializer for Generic Custom Lists

In many Kotlin projects, I am using the NonEmptyList from arrow-kt for improved type safety. A problem arises when I want to serialize and deserialize such lists.

My attempt at a generic custom serializer with delegation does not seem to work:

class NonEmptyListSerializer<T: Serializable>() : KSerializer<NonEmptyList<T>> {
    private val delegatedSerializer = ListSerializer(T::class.serializer())

    @OptIn(ExperimentalSerializationApi::class)
    override val descriptor = SerialDescriptor("NonEmptyList", delegatedSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: NonEmptyList<T>) {
        val l = value.toList()
        encoder.encodeSerializableValue(delegatedSerializer, l)
    }

    override fun deserialize(decoder: Decoder): NonEmptyList<T> {
        val l = decoder.decodeSerializableValue(delegatedSerializer)
        return NonEmptyList.fromListUnsafe(l)
    }
}

The problem is that I cannot create the delegatedSerializer from the type parameter T, because type information is erased. Reification does not work for classes. Is there any possibility to access the serializer of the list's base object?

Upvotes: 1

Views: 1325

Answers (2)

Ulrich Schuster
Ulrich Schuster

Reputation: 1916

This is how I got it working:

class NonEmptyListSerializer<T>(baseTypeSerializer: KSerializer<T>) : KSerializer<NonEmptyList<T>> {
    private val delegatedSerializer = ListSerializer(baseTypeSerializer)

    @OptIn(ExperimentalSerializationApi::class)
    override val descriptor = SerialDescriptor("EnergyTimeSeries", delegatedSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: NonEmptyList<T>) {
        val l = value.toList()
        encoder.encodeSerializableValue(delegatedSerializer, l)
    }

    override fun deserialize(decoder: Decoder): NonEmptyList<T> {
        val l = decoder.decodeSerializableValue(delegatedSerializer)
        return l.toNonEmptyListOrNull() ?: throw IndexOutOfBoundsException("Nonempty list to be deserialized is empty")
    }
}

The trick is to pass in the base type serializer as constructor parameter. When annotating a type with @Serializable(with = NonEmptyListSerializer::class), the serialization plugin seems to automatically provides the correct paramter. I am using Kotlin 1.7.20; not sure if it works also for older Kotlin releases.

Could someone explain exactly why the solution works?

Upvotes: 1

Sandy
Sandy

Reputation: 1

You can use a companion object with an inline operator fun invoke to create a pseudo-constructor with a reified type parameter.

class NonEmptyListSerializer<T : Serializable>(
    val delegatedSerializer: SerialDescriptor
) : KSerializer<NonEmptyList<T>> {

    companion object {
       inline operator fun <reified T> invoke() = NonEmptyListSerializer(ListSerializer(T::class.serializer()))
    }
    
    override val descriptor = SerialDescriptor("NonEmptyList", delegatedSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: NonEmptyList<T>) {
        val l = value.toList()
        encoder.encodeSerializableValue(delegatedSerializer, l)
    }

    override fun deserialize(decoder: Decoder): NonEmptyList<T> {
        val l = decoder.decodeSerializableValue(delegatedSerializer)
        return NonEmptyList.fromListUnsafe(l)
    }

}

Upvotes: -1

Related Questions