Trying kotlin 1.4.32 / serialization 1.1.0 (and kotlin 1.5.0 / serialization 1.2.0) I could not find a way to serialize the following hierarchy of classes
interface Range<T:Comparable<T>>
@Serializable @SerialName("range")
class SimpleRange<T:Comparable<T>>: Range<T>
@Serializable @SerialName("multirange")
class MultiRange<T:Comparable<T>>: Range<T>
I could serialize a SimpleRange<Double> (declared as a Range<Double>) with a SerializersModule including
polymorphic(Range::class) {
subclass(SimpleRange.serializer(Double.serializer()))
}
But I could not find a way to configure the module in such a way that it can serialize/deserialize a SingleRange<Double> or a SingleRange<Int> or a MultiRange<String> or any combination I could declare in the SerializersModule.
For example if I add subclass(SimpleRange.serializer(Int.serializer()) to the previous, I get a SerializerAlreadyRegisteredException
For dummy classes without generic fields this should be enough:
val module = SerializersModule {
polymorphic(Range::class) {
subclass(SimpleRange.serializer(PolymorphicSerializer(Comparable::class)))
subclass(MultiRange.serializer(PolymorphicSerializer(Comparable::class)))
}
}
But if you have a fields with type T : Comparable<T> in these classes, then it's more tricky. Generally, you would need to declare polymorphic serializer for Comparable interface too, but the problem is that you can't register "primitive types" (String, Int, Double, etc.) as subclasses for polymorphic serialization (limitation of kotlinx.serialization). And these types are crutial in this case.
As a workaround you may do the following:
@ExperimentalSerializationApi
val module = SerializersModule {
contextual(Comparable::class) { it[0] } //use serializer of its first type parameter
}
T type as Comparable<T> (essentialy it's the same in this case, but otherwise you'll get cryptic error message java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0) and mark them with @Contextual annotation:@Serializable
@SerialName("range")
class SimpleRange<T : Comparable<T>>(@Contextual val min: Comparable<T>, @Contextual val max: Comparable<T>) : Range<T>
//Will not work due to loss of information about type parameter
polymorphic(Range::class) {
subclass(SimpleRange.serializer(ContextualSerializer(Comparable::class)))
subclass(MultiRange.serializer(ContextualSerializer(Comparable::class)))
}
So, in order to preserve type parameter information, subclass serializer should be selected manually via auxiliary function with reified type paramters:
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Range<K>, reified K : Comparable<K>> rangeSerializer(value: T): KSerializer<T> = when (value) {
is SimpleRange<*> -> serializer<SimpleRange<K>>()
is MultiRange<*> -> serializer<MultiRange<K>>()
else -> throw NotImplementedError() // still required even for sealed interfaces, KT-21908
} as KSerializer<T>
Usage:
@ExperimentalSerializationApi
fun main() {
val kotlinx = Json {
serializersModule = module
}
val range1: SimpleRange<Int> = SimpleRange(1, 2)
println(kotlinx.encodeToString(rangeSerializer(range1), range1)) // {"min":1,"max":2}
val range2: SimpleRange<Double> = SimpleRange(1.0, 2.0)
println(kotlinx.encodeToString(rangeSerializer(range2), range2)) // {"min":1.0,"max":2.0}
val range3: Range<Double> = range2
println(kotlinx.encodeToString(rangeSerializer(range3), range3)) // {"min":1.0,"max":2.0}
}
(Deserialization)
Since we abandoned polymorphic serialization when serialized this data, there is no class discriminator field in the resulting JSON to figure out what subclass of Range should be instantiated. But even if it were there, it's not enough - we need class discriminator for Comparable as well.
So, we need some heuristics to implement content-based polymorphic deserialization.
For the sake of these heuristics simplicity, I'd suggest to add dedicated fields for this: type (could be configured with classDiscriminator property) and comparableType respectfully. So, serializer should be tweaked:
@ExperimentalSerializationApi
inline fun <reified T : Range<K>, reified K : Comparable<K>> rangeSerializer(value: T): SerializationStrategy<T> =
object : SerializationStrategy<T> {
@Suppress("UNCHECKED_CAST")
private val rangeSerializer = when (value) {
is SimpleRange<*> -> serializer<SimpleRange<K>>()
is MultiRange<*> -> serializer<MultiRange<K>>()
else -> throw NotImplementedError() // still required even for sealed interfaces, KT-21908
} as KSerializer<T>
override val descriptor = rangeSerializer.descriptor
override fun serialize(encoder: Encoder, value: T) = with(encoder as JsonEncoder) {
val jsonElement = json.encodeToJsonElement(rangeSerializer, value)
encodeJsonElement(transformSerialize(jsonElement, json))
}
private fun transformSerialize(element: JsonElement, json: Json) = JsonObject(element.jsonObject.toMutableMap().also {
val typeSerialName = value::class.findAnnotation<SerialName>()?.value ?: value::class.simpleName!!
it[json.configuration.classDiscriminator] = JsonPrimitive(typeSerialName)
it["comparableType"] = JsonPrimitive(K::class.simpleName)
})
}
Now, respectful polymorhic deserializer could be declared:
@ExperimentalSerializationApi
@ExperimentalStdlibApi
object RangeDeserializer : DeserializationStrategy<Range<*>> {
private val comparableSerializers = buildMap {
registerSerializerFor<Int>()
registerSerializerFor<Double>()
registerSerializerFor<String>()
}
@InternalSerializationApi
override val descriptor = buildSerialDescriptor("RangeDeserializer", PolymorphicKind.SEALED)
override fun deserialize(decoder: Decoder): Range<*> = with(decoder as JsonDecoder) {
val jsonObject = decodeJsonElement().jsonObject
val deserializer = selectDeserializer(jsonObject, json)
json.decodeFromJsonElement(deserializer, transformDeserialize(jsonObject, json))
}
private fun selectDeserializer(jsonObject: JsonObject, json: Json): DeserializationStrategy<out Range<*>> {
val type = jsonObject[json.configuration.classDiscriminator]!!.jsonPrimitive.content
val comparableType = jsonObject["comparableType"]!!.jsonPrimitive.content
val comparableSerializer = comparableSerializers[comparableType]!!
return when (type) {
"range" -> SimpleRange.serializer(comparableSerializer)
else -> MultiRange.serializer(comparableSerializer)
}
}
//remove extra fields (which we introduced as heuristics for actual serializer selection) before further JSON processing to avoid requiring `ignoreUnknownKeys = true`
private fun transformDeserialize(jsonObject: JsonObject, json: Json) = JsonObject(jsonObject.toMutableMap().also {
it.remove(json.configuration.classDiscriminator)
it.remove("comparableType")
})
}
private inline fun <reified T : Comparable<T>> MutableMap<String, KSerializer<*>>.registerSerializerFor() =
put(T::class.simpleName!!, serializer<T>())
Usage:
@ExperimentalSerializationApi
@ExperimentalStdlibApi
fun main() {
val kotlinx = Json {
serializersModule = module
}
val range1: SimpleRange<Int> = SimpleRange(1, 2)
val encoded1 = kotlinx.encodeToString(rangeSerializer(range1), range1)
println(encoded1) // {"min":1,"max":2,"type":"range","comparableType":"Int"}
val range2: SimpleRange<Double> = SimpleRange(1.0, 2.0)
val encoded2 = kotlinx.encodeToString(rangeSerializer(range2), range2)
println(encoded2) // {"min":1.0,"max":2.0,"type":"range","comparableType":"Double"}
val range3: Range<Double> = range2
println(kotlinx.encodeToString(rangeSerializer(range3), range3)) // {"min":1.0,"max":2.0,"type":"range","comparableType":"Double"}
val range1Decoded: Range<*> =
kotlinx.decodeFromString(RangeDeserializer, encoded1) // SimpleRange(min=1, max=2)
val range2Decoded: Range<*> =
kotlinx.decodeFromString(RangeDeserializer, encoded2) // SimpleRange(min=1.0, max=2.0)
}
If you can come up with better heuristics for actual serializers selection (based solely on shape of the original JSON, without introducing extra fields), then you can retain original serializer and enjoy more concise JSON.
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