Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override global Json.NET enum handling for one particular enum via decoration

I have a bunch of enums that I want Json.NET to serialize as camelcased strings. I have the following in my Global.asax.cs file and it's working great:

HttpConfiguration config = GlobalConfiguration.Configuration;
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter { CamelCaseText = true });

This makes it so an enum like this:

public enum FavoriteWebSite {
    StackOverflow,
    GoogleNews
    // Etc
}

will serialize to values like "stackOverflow", "googleNews", etc.

However, I have a couple enums that are bitwise masks. To make this a simple example, suppose one looks like this:

public enum Hobbies {
    Walking = 0x01,
    Biking = 0x02,
    // Etc
}

What happens when I serialize instances of this enum depends on what kind of values are in it. For example:

Hobbies set1 = Hobbies.Walking;                  // Serializes as "walking" -- bad
Hobbies set2 = Hobbies.Walking | Hobbies.Biking; // Serializes as "3"       -- good!

I want to override the serialization on this enum to just serialize as an int, while leaving the global setting to use camelcased strings intact.

I tried removing the global configuration so that enums by default are serialized as ints, then adding only [JsonConverter(typeof(StringEnumConverter))] to the non-bitmask enums. However, this results in PascalCased, rather than CamelCased serialization for those. I didn't see any way to get the CamelCaseText attribute set when using StringEnumConverter in a method decoration like above.

So, to recap, the goal is:

  1. Have single-value enums be serialized as pascalCased strings.
  2. Have bitmask enums be serialized as ints.

Thank you!

like image 365
Brian Rak Avatar asked Apr 10 '15 23:04

Brian Rak


People also ask

What is JsonProperty annotation C#?

JsonPropertyAttribute indicates that a property should be serialized when member serialization is set to opt-in. It includes non-public properties in serialization and deserialization. It can be used to customize type name, reference, null, and default value handling for the property value.

What is Stringenumconverter?

Converts an Enum to and from its name string value. Newtonsoft.Json. JsonConverter. Newtonsoft.Json.Converters.


2 Answers

Your main difficulty appears to be that you are not decorating your flag enums with FlagsAttribute, like so:

[Flags]
public enum Hobbies
{
    Walking = 0x01,
    Biking = 0x02,
    // Etc
}

This is the recommended best practice for flag enums:

Designing Flag Enums

√ DO apply the System.FlagsAttribute to flag enums. Do not apply this attribute to simple enums.

See also here. If you don't do this, many enum-related .Net utilities may not work as expected for flag enumerations.

Having done this, StringEnumConverter will serialize flag enums with composite values as a set of comma-separated values instead of as the numeric value you are currently seeing:

{
  "Hobbies": "walking, biking"
}

If you don't want this and still prefer to see default, numeric values for flag enums in your JSON, you can subclass StringEnumConverter to only convert non-flag enums:

public class NonFlagStringEnumConverter : StringEnumConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (!base.CanConvert(objectType))
            return false;
        return !HasFlagsAttribute(objectType);
    }

    static bool HasFlagsAttribute(Type objectType) 
    { 
        return Attribute.IsDefined(Nullable.GetUnderlyingType(objectType) ?? objectType, typeof(System.FlagsAttribute));
    }
}

Then use it like:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new NonFlagStringEnumConverter  { CamelCaseText = true });

This will cause Json.NET to fall back on any global default JSON converter for enums, or to numeric serialization if there is no applicable fallback. Demo fiddle #1 here.

Additionally, if you need to supersede a converter applied at a higher level and force numeric serialization for flag enums, use the following:

public class ForceNumericFlagEnumConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (!(Nullable.GetUnderlyingType(objectType) ?? objectType).IsEnum)
            return false;
        return HasFlagsAttribute(objectType);
    }

    public override bool CanRead { get { return false; } }

    public override bool CanWrite { get { return false; } }

    static bool HasFlagsAttribute(Type objectType) 
    { 
        return Attribute.IsDefined(Nullable.GetUnderlyingType(objectType) ?? objectType, typeof(System.FlagsAttribute));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Demo fiddle #2 here.

like image 107
dbc Avatar answered Oct 06 '22 15:10

dbc


This blog post explains pretty well that there is not built in way to override global StringEnumConverter. You need write you own converter that does nothing, then when converting JSON.NET will go back to default converter for that type (which for enums is serializing to it's numeric value).

So in case you have global converter:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter { CamelCaseText = true });

You can define this ForceDefaultConverter converter

public class ForceDefaultConverter : JsonConverter
{
    public override bool CanRead => false;
    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) => throw new NotImplementedException();
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}

And use it on the property you want to override your default StringEnumConverter.

public class ExampleDto
{
    [JsonConverter(typeof(ForceDefaultConverter))]
    public TestEnum EnumValue { get; set; }
}

or on enum type itself if you want this numeric values when serializing all objects with this enum type.

[JsonConverter(typeof(ForceDefaultConverter))]
public enum TestEnum
{
    Foo = 1,
    Bar = 2
}
like image 38
Mariusz Pawelski Avatar answered Oct 06 '22 15:10

Mariusz Pawelski