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