So far, this library has been working perfectly to serialize from/to Kotlin clients/servers. We are now trying to consume the JSON output in Dart.
As part of doing so, I received a request from the developer implementing the Dart client to always add the class discriminator to JSON objects, i.e., also when no polymorphic serializer is used. I believe doing so for the top-level object being serialized is possible by specifying PolymorphicSerializer explicitly, but this would not work recursively. Furthermore, I guess this would require all objects to be registered as polymorphic in SerialModule.
I personally don't dislike the approach taken by kotlinx.serialization to only add the class discriminator in case discriminating between multiple classes is needed, i.e., when you don't know which one of multiple classes are expected at a specific position in the JSON tree. On the other hand, I understand where the request comes from: the serialization library in Dart maintained by this developer does not have the expected type information from the call site and thus always needs the class discriminator.
Would this be a configuration option which is useful to more people?
How would I go about supporting this myself for the time being without registering all types as polymorphic? We use the same codebase for two separate infrastructures: one wants to use default kotlinx.serialization JSON configuration options, the other is now asking to add these additional class descriptors. I am considering a custom serializer akin to PolymorphicSerializer which is applied recursively and adds/ignores class discriminators where needed and redirects to the default serializers.
I looked into different options of supporting this myself (question 2).
Given that the serializers are generated per class and registered statically (at compile time), it is not straightforward to apply recursion to modify all serializers encountered as part of a full object tree. I basically need to conditionally 'replace' all serializers and defer to a custom serializer which adds the class discriminator if needed. As part of this, I believe I would have to look at the serializer descriptors and depending on the information stored therein defer serialization of basic types to the default serializers, and wrap all 'class' serializers with my custom serializer recursively. I feel I would have to rewrite quite a bit of object hierarchy traversal (arrays/maps/etc.).
Instead, I also looked at the kotlinx.serialization source code to determine how much work it would be to implement this as a JSON configuration feature. At a glance this would be much easier to implement:
if (writePolymorphic) {
writePolymorphic = false
encodeTypeInfo(descriptor)
}
... could probably be rewritten akin to if (writePolymorphic || json.configuration.alwaysWritePolymorphic). Although there is also array polymorphism to be taken into account.
What are your thoughts on this?
Indeed, this may be easy. Although this feature looks quite exotic for me — are there any use-cases for it besides Dart interop? Why Dart itself can't read classes without fq names?
I haven't looked into the details of the specific serialization library used in Dart myself yet (I think built_value), but the generalized use case would be to simplify interop with serialization libraries less advanced than kotlinx-serialization.
The discrepancy, as I currently understand it, lies in the lack of statically available information which can determine when to look or not to look for a polymorphic serializer. In kotlinx.serialization this is available in the descriptor. I'm guessing there is no such equivalent in the Dart serializer, and potentially other serialization libraries in other languages. An easy fallback would then be to always look for a class discriminator to determine which serializer to use and implement some central serializer key/value store (similar to SerialModule).
Of course, at the call site the deserialized object would still need to be cast to a type safe field, so I am not 100% certain how they currently expect to deal with that. I can investigate further; right now I do not have more information.
Maybe what I really want is Dart as a target platform for Kotlin. ;p
Just chipping in here: I was the one raising this issue to @Whathecode since I'm consuming the json objects on the Dart side. The main problem is that in order to know which Dart class should be used for de-serialization, I need to know the type of this class. And I was expecting this to be available in the $type field. And this is independent of whether this is a polymorphic class or not.
Maybe what I really want is Dart as a target platform for Kotlin. ;p
YES - this would be highly appreciated...
Chiming in with a 'real-world' use case. Slack's API defines a hierarchy of common json structure - one of the most atomic is the text object, which can be either plain text or markdown: https://api.slack.com/reference/block-kit/composition-objects#text
This naturally fits well into a sealed class, and works great. However, elsewhere in the API, a 'text object' is expected that will _only_ be of type plain_text (or vice versa): e.g. https://api.slack.com/reference/block-kit/blocks#image_fields - so, naturally, when constructing higher level objects that contain these atomic pieces, you would restrict them to the allowed types. But, as soon as you do so, you'll no longer emit valid JSON (according to Slack) - they still require the type key, even though it's always going to be the same value. While, admittedly, it would be great if you didn't have to, it's not practical when dealing with a third party API.
I was able to workaround this using manual serializers following the pattern below, but it would definitely be handy to have this as a 'global' config option - as it is, I have to at least include this serializer on each file at the top level.
companion object DiscriminatorIncludingSerializer : KSerializer<PlainTextObject> {
override val descriptor = serializer().descriptor
override fun deserialize(decoder: Decoder) = serializer().deserialize(decoder)
override fun serialize(encoder: Encoder, value: PlainTextObject) {
encoder.encodeSerializableValue(TextObject.serializer(), value)
}
}
@paul-griffith I think this is a more specific issue because it only concerns concrete subtypes of polymorphic types.
This is the "inconsistency" problem mentioned in https://github.com/Kotlin/kotlinx.serialization/issues/1194.
Basically, kotlinx serialization doesn't bother adding the discriminator for known concrete types, event when we know they are subtypes of serializable polymorphic types. I think it would be very important and useful to have the option to always include the discriminator at least for these cases, so that the same class is always serialized the same way.
The current issue here is about adding a discriminator for every type. Which a generalization of this, but is a bit different.
I wouldn't be opposed to have such an option as well, but I would really like to have an intermediate option that enables discriminators only on subtypes of polymorphic serializable types.
Most helpful comment
@paul-griffith I think this is a more specific issue because it only concerns concrete subtypes of polymorphic types.
This is the "inconsistency" problem mentioned in https://github.com/Kotlin/kotlinx.serialization/issues/1194.
Basically, kotlinx serialization doesn't bother adding the discriminator for known concrete types, event when we know they are subtypes of serializable polymorphic types. I think it would be very important and useful to have the option to always include the discriminator at least for these cases, so that the same class is always serialized the same way.
The current issue here is about adding a discriminator for every type. Which a generalization of this, but is a bit different.
I wouldn't be opposed to have such an option as well, but I would really like to have an intermediate option that enables discriminators only on subtypes of polymorphic serializable types.