Priority of JSON converter attributes

July 14th 2023 .NET Serialization

After I learned about JsonStringEnumMemberConverter, I wanted to use it for an enum in a large project I was working with, but to my surprise it didn't seem to have any effect on the serialization.

I added a JsonConverter attribute to the enum and the EnumMember attribute to all the values, just as I did during my original research:

[JsonConverter(typeof(JsonStringEnumMemberConverter))]
public enum MemberEnum
{
    [EnumMember(Value = "Value One")]
    ValueOne = 1,
    [EnumMember(Value = "Value Two")]
    ValueTwo = 2,
}

I included the enum as a property in my object:

public class SampleObject
{
    public NameEnum NameValue { get; set; }
    public MemberEnum MemberValue { get; set; }
}

And returned an instance from the action method:

[HttpGet(Name = "GetSample")]
public SampleObject Get()
{
    return new SampleObject
    {
        NameValue = NameEnum.ValueA,
        MemberValue = MemberEnum.ValueTwo,
    };
}

However, the JSON returned by the endpoint still ignored the Value in the EnumMember attribute:

{
  "nameValue": "ValueA",
  "memberValue": "ValueTwo"
}

After some troubleshooting, I figured out that the reason for this behavior was the JsonStringEnumConverter that was added to the JsonSerializerOptions:

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    });

I originally added this converter to the options so that didn't have to add the JsonConverter attribute to all the enums in my project. But I incorrectly expected that adding a JsonConverter attribute with a different converter would override the one from JsonSerializerOptions. Well, it doesn't, and this behavior is documented:

When placed on a property, the specified converter will always be used.

When placed on a type, the specified converter will be used unless a compatible converter is added to the JsonSerializerOptions.Converters collection, or there is another JsonConverterAttribute on a property of the same type.

One way to fix the behavior would be by adding the JsonConverter attribute to the class property instead of to the enum. But I didn't like that approach because it's error-prone: I could easily forget to add the attribute when I created another property of the same type. On top of that, the generated Swagger/OpenAPI specification would still show incorrect enum values.

Fortunately, the JsonStringEnumMemberConverter behaves the same as JsonStringEnumConverter when there is no EnumMember attribute on the enum value. That was good enough for me: I wanted the Value from the EnumMember attribute to be used for serialization when it's present. This meant that I could fix my issue by changing the converter in JsonSerializerOptions:

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(
            new JsonStringEnumMemberConverter());
    });

Once I did that, there was also no need for adding the JsonConverter attribute to my enum type anymore:

public enum MemberEnum
{
    [EnumMember(Value = "Value One")]
    ValueOne = 1,
    [EnumMember(Value = "Value Two")]
    ValueTwo = 2,
}

And the endpoint returned the JSON that I wanted it to:

{
  "nameValue": "ValueA",
  "memberValue": "Value Two"
}

You can find a working sample project using this approach in my GitHub repository. The previous commit shows that adding a JsonConverter to a type doesn't override the converter from JsonSerializerOptions.

It's dangerous to assume behavior of frameworks you are using. You should always check the documentation or at least test that the code works as you expect it to. No matter how intuitive it felt to me that a JsonConverter attribute on a type would override the one from JsonSerializerOptions, it turned out that it doesn't work like that.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License