-
Notifications
You must be signed in to change notification settings - Fork 624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow custom policy for adding the polymorphic discriminator #1247
Comments
Thank you for this excellent summary and aggregating a set of diverse yet related issues! I just wanted to note that the current two ways polymorphism is supported and implemented (normal and array polymorphism) might complicate this suggestion; I haven't given it much thought, though, and I expect it is certainly something the smart people behind this library could tackle. ;p For example, a recent issue I ran into is that adding a class discriminator to JSON primitives currently only works when array polymorphism is used. I had an Enum which extended from an interface and wanted to serialize this polymorphically, for which I had to create a custom serializer. This is yet another reason why I still prefer kotlinx.serialization's original proposal to use array polymorphism by default, but I understand this deviates from 'the standard', if that even exists. |
Any progress on it? |
As for now, there is sufficient demand and use-cases to provide an option to globally disable type discriminator (NEVER option) — mainly, to interact with external API that has validation or just to reduce the amount of data.
So for now we are leaning towards a design with only two states: DEFAULT (current behavior) and NEVER (do not include discriminator at all). If you have any additional suggestions or corrections, please add them |
@sandwwraith Thanks for the summary of the state of this issue.
I actually think the opposite is true sometimes. The current way of serializing surprises some people because the same class will not be serialized the same way depending on whether the serializer used was that of the parent or that of the child class. The proposal here is to ensure both serializers yield the same result for a given instance. So IMO this rule (which ensures consistent serialization of a given instance) is actually easier to understand than the current default behaviour.
I understand that for most use cases, it's just a failure of users to understand how the serializer is chosen (based on the static type of the variable at the call site). For these cases #1493 would be sufficient. However, there are some use cases where the call site is not controlled, and for those it's not about fixing the call site, it's just that some frameworks (e.g. Spring) have a generic API surface on top of different serialization libraries, and they don't have any static type information at the moment they call the concrete serialization library, just the runtime type of the instances to serialize. For example, anywhere in the code, you could have a call like this: simpMessagingTemplate.convertAndSend("/some/destination", someObject) This public API is generic for Spring, regardless of the serialization library used. Once deep down in Spring implementation, the runtime type of A good suggestion by @pdvrieze to work around this issue on Spring side without changing their public API is to wrap the serialized object into an internal wrapper class known to Spring with additional static type information (e.g. the serializer), and unwrap this in the Kotlinx Serialization adapter.
I would have expected something at compiler-plugin level, because parent classes annotated |
The point about Spring and other frameworks is a really good one, thanks. Even Ktor has the same problem. It's still possible to use
This is true indeed. However, there is still a 'dynamic registration of polymorphic types' — a
This is an implementation detail: the discriminator is added by the format, not by the serializer. So we need to keep a 'default' serializer for child and do not use PolymorphicSerializer for it because this will lead to a stack overflow |
I'm not sure what you mean here. At least as far as this Spring function is concerned, this is not correct. This method doesn't have a type parameter. Spring entirely relies on the runtime type of the 2nd argument, once this "message payload" hits the actual message converter that will be used for serialization. This call site is long forgotten by then. The only way such a call could be made this way is if we defined an extension function with a reified type parameter, and invent a wrapper class that wraps the payload instance + the serializer to use. And then modify Spring's code in their Kotlinx Serialization adapter to detect this wrapper class, unwrap it, and read the serializer from there. That being said, I still consider it only as a workaround, because of all the unnecessary allocations of wrapper objects. If these messages are handled by the million, it could be a non-negligible overhead.
True, even building a reified version of such functions via the workaround mentioned above would actually not entirely solve the problem. Deserializing polymorphic instances on the other end of the communication should (IMO) not really depend on whether the program that did the serialization was using a polymorphic static type or a concrete subtype. I understand why this happens this way at the moment, but I feel like it would be nice to allow to customize this behaviour.
I see. I have only ever needed
Isn't the Based on this very limited and maybe incorrect understanding, the point I was making above was that the |
Just to clarify for the others, polymorphic (de)serialization, the (outer) polymorphic serializer that has 2 elements (the type discriminator, and the data element). The data element is then (de)serialized using the type specific serializer. The format (json when not in array format, xml in transparent mode) can elide this extra structure and use a different approach instead (type discriminator, type specific tag name). Basically it works as follows:
As such, without having a polymorphic serializer a format does not know how a type is used, or even what the base type is (there could be multiple valid parent types for the same polymorphic leaf type). For sealed serialization, there is only one serializer generated for child, this is the type specific (non-polymorphic) serializer. Then for any sealed parent (there could be multiple, hierarchic), this is always abstract and the descriptor contains the information of all the possible sub-serializers in the sealed hierarchy. The child adding a discriminator on its own is hard (and certainly doesn't fit the current architecture). The problem is that the format is then unable to know that the (synthetic) property is actually a type discriminator. More significant however is that the serializer itself cannot know the context of it's own use (polymorphic or static). It is perfectly possible for a format to add a discriminator for all structure types, it is also possible for it to create a set of typenames based upon the content of a SerializersModule (using the dumpTo function). This has performance drawbacks. As an alternative, it is also possible to "wrap" the serializers into remapping serializers that wrap all desired classes into a polymorphic/sealed wrapper. If you only care about the outer type, then it is even easier, just put it in a polymorphic (or sealed) parent when creating the serializer. |
While discussing @Serializable sealed class Base
@Serializable class Sub: Base()
@Serializable
data class Message(val b: Base, val s: Sub) In this case, On one hand, Maybe we overthink things a bit here, and there is almost no real-world usages of sub class as a property, but this fact is yet to be validated. So we need to determine whether it is reasonable to provide type information only on top-level or in all properties of subtypes. |
Oh, that is quite a valid concern.
I don't think it's overthinking. I find it quite reasonable to expect subtypes as property types, especially in the case of sealed hierarchies. For instance, you could imagine a sealed class I believe that the initial case I made for If it were designed this way, maybe a better name might be I guess providing the option of always including the discriminator for these subtypes would be nice for consistency and understandability, but I wonder if there is an actual use case for this. |
I'd like to have the option to change the default strategy from using the FQCN ( |
@efenderbosch The discriminator is determined by the |
Right, a runtime configurable strategy is what I had in mind. val myFormatter = Json {
descriminatorStrategy = SimpleClassnameStrategy::class
} |
Hey @sandwwraith, how about you just make |
What if I have inconsistent BE and I need exactly to specify to have or not discriminator for complex nested classes? |
My use case. I have a white label android application. It has a user profile that has different fields based on the specific label. I annotated
LabelA back end doesn't need a discriminator and throws an error about the unknown type when it is included. So I need a way to hide it. However, in the
Here I actually need to pass a discriminator to the back end. |
@emartynov For the |
I would like to add to this discussion from an Open API compliance standpoint. See the polymorphism example here. The problem with not always including the discriminator in the JSON is that it will no longer validate against the JSON schema since normally the discriminator property is marked as required in either the base class (included via I think it makes sense to at least be able to comply with the Open API spec, by providing an ALWAYS mode for class hierarchies. |
I have a similar situation where I don't know the actual polymorphic hierarchy on serialization. The backend is serializing a NotAuthenticatedError, the client knows that this endpoint could potentially return the successful model or the NotAuthenticatedError and treats it as a sealed class, but the backend is throwing the exception before the controller is called and as such has no way to figure out the specific Implementation of NotAuthenticated it should serialize, so it just makes sure that the properties match the client's expectation, except that we need the class discriminator... So I would also greatly appreciate the ability to always include the class discriminator. I have no idea how to work around this issue at the moment. Maybe I can just add every type as a polymorphic serialization option for Any and serialize everything as Any? Not ideal, and I don't know that it works with Spring (which throws away any attempt to use kotlinx polymorphically). |
To add another valid usecase for some kind of "always discriminate" configuration, GeoJSON requires that all objects have a It would be very handy to leverage the in-built type discrimination to make encoding/decoding compliant to the spec. To workaround this, I am adding an |
In my case, I'm trying to use a base class to define optional fields that can be sent to a logging API. Each section of the app may use a subset of these fields. For example, the HTTP module is likely to use the http logging fields, but those won't be used by any other module. It's beneficial to keep them all on a base interface to prevent modules from accidentally using reserved field names. I only need to serialize, not deserialize, so I don't care about polymorphism. However, this throws the error It would be great if we could turn off polymorphism for certain use cases. @Serializable
abstract class DataDogBaseLog {
// Require a message.
abstract val message: String
// Apply a default timestampe here so that we don't have to include this in every subclass
@Serializable(with = InstantSerializer::class)
val date: Instant = Instant.now()
// Use "Transient" to prevent this from being sent unless overridden.
@Transient
open val host: String = ""
// other fields
} In another module: @Serializable
data class ModuleLog(
// Reserved properties
override val message: String,
override val host: String,
// Module-specific properties
val prop1: String,
val prop2: Int,
): DataDogBaseLog() |
@distinctdan The error message you get means that you have a variable of (static) type If you want to make this work, it may work making |
Thanks for the explanation @pdvrieze, that makes sense about the base being abstract. That's an interesting idea of making the child class However, I have gotten inheritance to work by defining a custom polymorphic serializer that picks the base class. This produces the following behavior:
I haven't been able to get default values in the base class to be serialized, but this is good enough for me for now.
|
There's a prototype for this feature in #2450; feel free to review it. The current problem is that implementing |
Implement ClassDiscriminatorMode.ALL, .NONE, and .POLYMORPHIC As a part of the solution for #1247
Any update ? |
@Ayfri See comments in the linked PR. |
So if I understand correctly, it will be fixed by the next release as the pull request is already merged ? |
Mostly, yes. |
Good to know ! Is there any milestone for the next release ? |
Seems it has been released in https://github.com/Kotlin/kotlinx.serialization/releases/tag/v1.6.3 🎉 |
Is this still the thread to follow for |
What is your use-case and why do you need this feature?
There has been several use cases mentioned in different issues that require to add the discriminator in more places, or avoid it in some places:
Describe the solution you'd like
It seems these use cases could be supported by having a configuration parameter for the inclusion of the discriminator, such as
discriminatorInclusionPolicy
, with the following values:NEVER
: never include the discriminatorPOLYMORPHIC_TYPES
(current behaviour): include the discriminator only when serializing a type that's polymorphic itself (the "parent" type).POLYMORPHIC_SUBTYPES
: always include the discriminator for instances (subtypes) of polymorphic types that are serializable, regardless of whether they are used as their parent or as themselves (json.encodeToString<Parent>(SubType())
andjson.encodeToString<SubType>(SubType())
would behave the same way).ALWAYS
,ALL_TYPES
, orALL_OBJECT_TYPES
: always include the discriminator in types serialized as JSON objects (does not apply to primitive types)Maybe the names could be refined, but I think you get the idea.
Another option could also be to provide a custom function that tells whether to include the discriminator or not, but then we would have to think about which pieces of information the function would need etc.
Any thoughts on this?
The text was updated successfully, but these errors were encountered: