8

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!

Brian Rak
  • 4,912
  • 6
  • 34
  • 44
  • So, you do want some enums to be serialized as `string` and some as `int`, right? This comment: `// Serializes as "walking" -- bad` made me think that you want all of them to be serialized as `int`. – ataravati Apr 11 '15 at 21:26
  • By the way, goal number 1 is for the single-value enums to be serialized as camelCased strings, not pascalCased. Am I right? – ataravati Apr 11 '15 at 21:27

2 Answers2

5

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • this didnt worked for me, flags are still converted to string names – AFgone May 11 '20 at 14:56
  • I was not able to add json.net add reference but my code is like this; https://dotnetfiddle.net/Yu2CRZ – AFgone May 11 '20 at 15:17
  • 1
    @AFgone - I forked your fiddle here: https://dotnetfiddle.net/S52HB4, and I can't see a problem. The `UserCan ability` object is being serialized as an integer `5` which is the required behavior for this converter: `[Flags]` enums to be serialized as integers, others as strings. Maybe you have some global serializer settings that contain the "regular" string enum converter? See [Json.net global settings](https://stackoverflow.com/a/16439024). – dbc May 11 '20 at 15:23
  • Yes I have global default settings however even if I provide new settings at that call isn't it used for that serialization? – AFgone May 11 '20 at 15:26
  • 1
    @AFgone - No, if `CanConvert(Type objectType)` returns `false` from the locally provided converter then Json.NET falls back on the global converter. Can you fork and update the fiddle to provide a [mcve]? – dbc May 11 '20 at 15:33
  • 1
    you are rirght, I used JsonSerializer.Create with settings and it worked. Many thanks for your help @dbc – AFgone May 11 '20 at 15:41
  • Hopefully last question, I forked it. It is not working for "doeverything" any helps would be appreciated. https://dotnetfiddle.net/air5pO – AFgone May 11 '20 at 16:36
  • 1
    You should ask a separate question, but this appears to be a known bug with Json.NET: [Failure to deserialize ulong enum #2301](https://github.com/JamesNK/Newtonsoft.Json/issues/2301). (The value of `UserCan.DoEverything` is actually `ulong.MaxValue`.) – dbc May 11 '20 at 16:45
  • 1
    @AFgone - added update about superseding a global default converter. – dbc May 11 '20 at 19:44
  • so you resolved it? You are great! I'm asking another question as it was a big headache for me, and in this way more people can benefit from it. Please post this explainin what you do so that I accept as answer. here is the other question https://stackoverflow.com/questions/61740964/json-net-unable-to-deserialize-ulong-flag-type-enum @dbc – AFgone May 11 '20 at 23:08
  • @AFgone - I resolved the issue with superseding a global converter here. As for the other `ulong`-related issue, it's completely unrelated, and seems specific to using a `ulong` as the underlying type for an `enum`. You might want to edit that question to include a [mcve] in the question itself. – dbc May 12 '20 at 00:32
3

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
}
Mariusz Pawelski
  • 25,983
  • 11
  • 67
  • 80