-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
JsonConverter precedence differs from Newtonsoft and may be unintuitive #1130
Comments
I'm guessing this was to enable configuring output-specific converter overrides, and its placement was to strike a balance between usability for fields and types. Although it does look a little strange to me. Perhaps what we should have instead is a way to override the converter for a type/field based on path? |
Can you elaborate a bit on this? I'm wondering if there is a legitimate use-case for that, and if there is, if it isn't better served through a different mechanism? As for overriding a converter based on path, can you show an example? I'm not sure if I'm picturing what you mean correctly. |
No, you might be right, because most of the instances I'm thinking of would either have converters on the fields (where it couldn't override it), or just have it configured on the output itself. For overriding based on path, you'd have something like |
I believe the intention behind this order of precedence is to make run-time changes override design-time choices. @steveharter - can you expand on the rational for the precedence order and is this something we should consider changing? |
Hmmm, wouldn't this defeat the purpose of what a
Sorry, do you mean that this is Newtonsoft's intention, or System.Text.Json's intention? From what I can see, Newtonsoft's precedence follows this intention, but System.Text.Json's doesn't. |
I think you're looking at the listing backwards - it's in order of precedence. That is, Newtonsoft's serializer-registered converters override nothing, but instead are providing "default" or fallback converters. |
@Clockwork-Muse ok I think I am finally understanding what you mean by "runtime" overrides (I put it in quotes because in actuality the scenario you're describing - overriding a third-party library - is still going to be specified at design time, unless you think that the string path is going to be set by some runtime configuration provider). That said:
|
Perhaps I more should have said "consume-time", instead of "run-time", because it's more about letting consumers override some of the behavior. |
Runtime has precedence over design-time since design-time normally comes first, and there is an intent to change that behavior with run-time settings. Here's some design notes from https://github.com/dotnet/corefx/issues/36639: Priority of converter registrationConverters can be registered at design-time vs run-time and at a property-level vs. class-level. The basic rules for priority over which is selected:
Thus the priority from highest to lowest:
|
If you have a custom converter factory that covers multiple types (and not just closing an open generic, which is really the primary intent) then yes you may want to check for the presence of the custom converter attribute. Normally I would not recommend that type of converter except for a low-level, work-around-missing-feature case. |
Thanks @steveharter. Maybe I should give our exact scenario as an example to showcase why I think this priority order may not be working as intended. We are using F#, and along with that we use Discriminated Unions. To support that, we have a custom So if we have these types: type SomeUnion =
| CaseA of string
| CaseB of string
type SomeType = {
Union: SomeUnion
} Then {
"union": {
"kind": "CaseA",
"value": "Some value"
}
} However, we need to provide the option for some union types to use different naming for the "kind" and "value" fields when de/serializing. To support this, we have a [<JsonUnionConverter(KindPropertyName = "Mode", ValuePropertyName = "Items")>]
type ModedFilter<'T> =
| ModedInclusion of 'T
| ModedExclusion of 'T However, this attribute will never be called by System.Text.Json because This is why I believe that despite the explanation in the design meeting, this approach may not be working as cleanly as intended. |
Linking https://github.com/dotnet/corefx/issues/38348 for F#; we should ensure the scenario above can be addressed with custom converters. There is also some community support for F# - see https://github.com/Tarmil/FSharp.SystemTextJson for an example. |
While my specific example dealt with an F#-specific scenario, this issue is not F#-specific. Also, adding built-in support for F# types into System.Text.Json will not alleviate the root cause of this problem, either. This F# scenario simply highlights the underlying problem, but it isn't the cause of it. |
@ylibrach so the run-time converter One option is to always use the attribute, and not register the factory. It may make it more difficult to use since I believe you'd have to use the attribute on every discriminated union. You may already be doing this, but to pass extra metadata to a converter through your own JsonConverter-attribute, override the attribute's CreateConverter() method and pass any state declared on the attribute to the converter's constructor. Currently I assume you are having the factory check for the presence of your attribute on the type, and if so, return "false" for CanConvert() so that the attribute works and creates the correct converter. I also assume this is working fine. Note that all the converters are cached for a particular Type and all of its properties, so the extra work to look for the attribute can be amortized over several (de)serializations of a given Type. |
FWIW another way to think about the original design:
Thus design-time is the "fallback" and run-time is the "override". |
Linking #29812
|
Closing as by-design behavior. |
Newtonsoft's precedence for converters is as follows (as defined by their docs for converters):
JsonConverter
defined by attribute on a memberJsonConverter
defined by an attribute on a classJsonSerializer
This seems to align pretty well with what I imagine is expected by most developers. However, the precedence used by System.Text.Json is different (as defined by the official docs for converters):
[JsonConverter]
applied to a property.Converters
collection.[JsonConverter]
applied to a custom value type or POCO.This was very unexpected and we found it to be unintuitive. I'm curious as to why an attribute defined on a specific class type would have less priority than a converter added to the global
Converters
collection? It seems to go against the general concept of specificity that developers are familiar with.In addition, this introduces a strange interaction between a
JsonConverter
/JsonConverterFactory
and aJsonConverterAttribute
, because it forces the converters to check for aJsonConverterAttribute
on the type they are de/serializing, which shouldn't be their responsibility:Even more, it forces a
JsonConverterAttribute
and aJsonConverterFactory
to have two differentCreateConverter
methods, instead of allowing the attribute to call a factory'sCreateConverter
. This is because the factory would need to call the attribute'sCreateConverter
if it exists on the type, thereby causing a circular call chain if the attribute'sCreateConverter
were to call back to the factory.The text was updated successfully, but these errors were encountered: