Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to serialize "Any" type in Kotlinx Serialization?

I have a class that gets serialized for network traffic.

@Serializable    
data class Packet(val dataType: String, val payload: Any)

I've used Java serialization to send it over the wire. The receiver can't know the type of the payload but Java deserializes it just fine, and then I use when(dataType) as a lookup to correctly cast the Any object to its correct type. Easy breazy.

But Kotlinx Serialization (with ProtoBuf) is a stickler about this Any type for reasons that aren't obvious to me. I can't register a serializer for Any. In the docs they recommend a polymorphic approach, which sorta works but you have to make the packet typed:

data class Packet<out T : Any>(val dataType: String, val payload: T) : SomeBaseClass<T>

but this kinda sucks because it weighs down a lot of code paths with inline reified typing, plus this doesn't solve that the receiving end won't know what type to try to deserialize the payload as without being to look at the dataType field first.

This is the worst catch-22. The framework won't ignore the payload: Any field (gives a compile error) and I can't even write a custom serializer because defining an element of type Any in a customer serializer (for the descriptor) gives the same run-time error of "no serializer registered for Any."

like image 909
honkbert Avatar asked Feb 11 '21 02:02

honkbert


People also ask

Can you serialize any object?

To serialize an object means to convert its state to a byte stream so way that the byte stream can be reverted back into a copy of the object. A Java object is serializable if its class or any of its superclasses implements either the java. io. Serializable interface or its subinterface, java.

Can we serialize every object in Java?

Only Classes That Implement Serializable Can Be Serialized Java's default serialization process is fully recursive, so whenever we try to serialize one object, the serialization process try to serialize all the fields (primitive and reference) with our class (except static and transient fields).

Is kotlin list serializable?

Unit and singleton objects The Kotlin builtin Unit type is also serializable.


Video Answer


1 Answers

I've used Java serialization to send it over the wire. The receiver can't know the type of the payload but Java deserializes it just fine, and then I use when(dataType) as a lookup to correctly cast the Any object to its correct type. Easy breazy.

This is because java serialization is rather primitive - there is only one way to serialize (and hence to deserialize) an object. In kotlinx.serialization each class can have its own serialization strategy (or even several ones). And this flexibility comes with a price. Serialization of Any could be handled (for declared list of its subclasses), but dynamic determintaion of deserialization strategy based on dataType field of partly deseriazed object is impossible in general case, because there is no guarantee that dataType field will be deserialized first. Some serialization formats (like JSON or Protobuf) have unordered schema. It could happen that payload is about to be deserialized before dataType, and Decoder interface doesn't allow to go back/make several passes.

If you're sure about the order of properties in your serialization format/message (or just feel lucky) you may go with the following custom serializer:

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

@Serializable(with = PacketSerializer::class)
data class Packet(val dataType: String, val payload: Any)

object PacketSerializer : KSerializer<Packet> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Packet") {
        element("dataType", serialDescriptor<String>())
        element("payload", buildClassSerialDescriptor("Any"))
    }

    @Suppress("UNCHECKED_CAST")
    private val dataTypeSerializers: Map<String, KSerializer<Any>> =
        mapOf(
            "String" to serializer<String>(),
            "Int" to serializer<Int>(),
            //list them all
        ).mapValues { (_, v) -> v as KSerializer<Any> }

    private fun getPayloadSerializer(dataType: String): KSerializer<Any> = dataTypeSerializers[dataType]
        ?: throw SerializationException("Serializer for class $dataType is not registered in PacketSerializer")

    override fun serialize(encoder: Encoder, value: Packet) {
        encoder.encodeStructure(descriptor) {
            encodeStringElement(descriptor, 0, value.dataType)
            encodeSerializableElement(descriptor, 1, getPayloadSerializer(value.dataType), value.payload)
        }
    }

    @ExperimentalSerializationApi
    override fun deserialize(decoder: Decoder): Packet = decoder.decodeStructure(descriptor) {
        if (decodeSequentially()) {
            val dataType = decodeStringElement(descriptor, 0)
            val payload = decodeSerializableElement(descriptor, 1, getPayloadSerializer(dataType))
            Packet(dataType, payload)
        } else {
            require(decodeElementIndex(descriptor) == 0) { "dataType field should precede payload field" }
            val dataType = decodeStringElement(descriptor, 0)
            val payload = when (val index = decodeElementIndex(descriptor)) {
                1 -> decodeSerializableElement(descriptor, 1, getPayloadSerializer(dataType))
                CompositeDecoder.DECODE_DONE -> throw SerializationException("payload field is missing")
                else -> error("Unexpected index: $index")
            }
            Packet(dataType, payload)
        }
    }
}

like image 129
Михаил Нафталь Avatar answered Oct 19 '22 12:10

Михаил Нафталь