Multi-format Mixed Primitive-Composite Polymorphism
What is your use-case and why do you need this feature?
I've created a small repo to demonstrate my use case: https://github.com/sugarmanz/mixed-primitive-composite-polymorphism
Essentially, I am trying to consume an API with a JSON spec that optionally allows some field to be specified as either a primitive or object:
{
"license": "ISC"
}
{
"license": {
"name": "ISC",
"url": "https://opensource.org/licenses/ISC"
}
}
I implemented a sealed class construct with some custom serializers to represent the different ways this data can be described:
https://github.com/sugarmanz/mixed-primitive-composite-polymorphism/blob/a2f014d6c3e8428e22f44140b56be443a7a1512e/src/main/kotlin/License.kt#L10-L55
I don't necessarily love the approach that I took for the base class deserializer, but I wasn't entirely sure how else to do it without introspecting the content. One of the goals with my library is to keep my serializers decoupled from a specific format, which means I can't use JsonContentPolymorphicSerializer to help. For the record, this does work for JSON only inputs:
object Serializer : JsonContentPolymorphicSerializer<License>(License::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out License> = when (element) {
is JsonPrimitive -> Primitive.serializer()
is JsonObject -> Boxed.serializer()
else -> throw SerializationException()
}
}
Regardless, this approach seems to work when deserializing the different representations as a single member. However, it seems to fail when attempting to deserialize a collection of the complex representation.
Expected class kotlinx.serialization.json.JsonObject as the serialized body of License.Boxed, but had class kotlinx.serialization.json.JsonArray
kotlinx.serialization.json.internal.JsonDecodingException: Expected class kotlinx.serialization.json.JsonObject as the serialized body of License.Boxed, but had class kotlinx.serialization.json.JsonArray
at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.beginStructure(TreeJsonDecoder.kt:347)
at License$Boxed$$serializer.deserialize(License.kt:32)
at License$Boxed$$serializer.deserialize(License.kt:32)
at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:51)
at License$Serializer.deserialize(License.kt:46)
at License$Serializer.deserialize(License.kt:36)
at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:51)
at kotlinx.serialization.internal.TaggedDecoder.decodeSerializableValue(Tagged.kt:206)
at kotlinx.serialization.internal.TaggedDecoder$decodeSerializableElement$1.invoke(Tagged.kt:279)
at kotlinx.serialization.internal.TaggedDecoder.tagBlock(Tagged.kt:296)
at kotlinx.serialization.internal.TaggedDecoder.decodeSerializableElement(Tagged.kt:279)
at kotlinx.serialization.encoding.CompositeDecoder$DefaultImpls.decodeSerializableElement$default(Decoding.kt:535)
at kotlinx.serialization.internal.ListLikeSerializer.readElement(CollectionSerializers.kt:80)
at kotlinx.serialization.internal.AbstractCollectionSerializer.readElement$default(CollectionSerializers.kt:51)
at kotlinx.serialization.internal.AbstractCollectionSerializer.merge(CollectionSerializers.kt:36)
at kotlinx.serialization.internal.AbstractCollectionSerializer.deserialize(CollectionSerializers.kt:43)
at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:51)
at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:24)
at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:119)
at LicenseTest.collection of primitive and objects(LicenseTest.kt:122)
Describe the solution you'd like
I'm not entirely sure what I would like to achieve is possible as-is, but would be thrilled to learn if I'm wrong. Otherwise, I think the best case solution, is being able to do content based polymorphism in a format agnostic manner. Maybe usingSerialKinds or something?
object Serializer : ContentPolymorphicSerializer<License>(License::class) {
override fun selectDeserializer(kind: SerialKind): DeserializationStrategy<out License> = when (kind) {
is PrimitiveKind -> Primitive.serializer()
is StructureKind -> Boxed.serializer()
else -> throw SerializationException()
}
}
Not sure if this is possible, or desired, but open to conversation.
For the multi-format part of your question, it is supported behaviour to detect whether an encoder/decoder passed to serialize/deserialize implements JsonEncoder/JsonDecoder or another format interface respectively. The implementation should have a sane default serialization, but can special case for the specific format.
As to the collection bug, the problem is that an array is not a box. You probably want to detect an array and use a Boxed.serializer.list() list serializer.
For the multi-format part of your question, it is supported behaviour to detect whether an encoder/decoder passed to serialize/deserialize implements JsonEncoder/JsonDecoder or another format interface respectively. The implementation should have a sane default serialization, but can special case for the specific format.
I guess "multi-format" isn't really the best term here. What I'd like to say is format agnostic. I'm just trying to create a library that provides serializable models for a specific construct on top of Kotlinx Serialization, of which boasts support for arbitrary formats. Tying my library to a specific format or formats is somewhat antithetical to the goal.
As to the collection bug, the problem is that an array is not a box. You probably want to detect an array and use a Boxed.serializer.list() list serializer.
Why should my serializer for License have to account for containing structures? Shouldn't that automatically be done by the fact that I'm passing in a ListSerializer(Licenser.serializer())?
val licenses: List<License> = Json.decodeFromJsonElement(
ListSerializer(License.serializer()), // ***
buildJsonArray {
// items...
}
)
By the time it tries to deserialize a single item, the JSON decoder should have already read the [?