Search code examples
kotlingenericsserializationreification

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?


Solution

  • 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?