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
."
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.
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).
Unit and singleton objects The Kotlin builtin Unit type is also serializable.
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)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With