Skip to content

JsonConverter precedence differs from Newtonsoft and may be unintuitive #1130

@ylibrach

Description

@ylibrach

Newtonsoft's precedence for converters is as follows (as defined by their docs for converters):

  • JsonConverter defined by attribute on a member
  • JsonConverter defined by an attribute on a class
  • any converters passed to the JsonSerializer

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.
  • A converter added to the 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 a JsonConverterAttribute, because it forces the converters to check for a JsonConverterAttribute on the type they are de/serializing, which shouldn't be their responsibility:

public class SomeConverter<T> : JsonConverter<T> {
    public SomeConverter(bool someOption) { ... }
}

public class SomeConverter : JsonConverterFactory {
    public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) {
        /* In order to be able to de/serialize `type` using `SomeConverterAttribute`, 
           we're forced to have this factory check if the attribute is set on `type`, 
           before creating `SomeConverter<T>`. */
    }
}

public class SomeConverterAttribute : JsonConverterAttribute {
    public bool SomeOption { get; set; }

    public override JsonConverter CreateConverter() {
        /* Create a `SomeConverter<T>` type, and call it with `SomeOption` */
    }
}

[SomeConverter(someOption = true)]
public class SomeClass { ... }

...

serializerOptions.Converters.Add(new SomeConverter());

Even more, it forces a JsonConverterAttribute and a JsonConverterFactory to have two different CreateConverter methods, instead of allowing the attribute to call a factory's CreateConverter. This is because the factory would need to call the attribute's CreateConverter if it exists on the type, thereby causing a circular call chain if the attribute's CreateConverter were to call back to the factory.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions