Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web Api incorrectly deserializing a list of enums

So, I'm using a web API controller to accept JSON requests. It is mapping to a model object that includes a list of enums. The problem I am having is that if the JSON includes an invalid value, it does not seem to be deserializing correctly. I would expect an invalid value to be mapped to the 0 value type in my enum list, however that is not happening.

There are 3 primary cases that I've isolated: If the JSON is in the form of

    ...
    "MyEnumList":["IncorrectEnum", "One", "Two"]
    ...

The value is not mapped at all, and I simply get a list with the two valid values. However, if I supply this JSON:

   ...
   "MyEnumList":["123", "One", "Two"]
   ...

I get a list with 3 objects, where the first object is of type "MyEnum" and has the value 123, even though that is not defined in my enum. The same happens if I supply this JSON syntax:

   ...
   "MyEnumList":[123, "One", "Two"]
   ...

Can anyone explain what is happening here, and how I can ensure that the values are always mapped to a valid type?

For reference, The model object, which contains a list of my enum:

    public class MyClass
    {
       public List<myEnum> MyEnumList { get; set; }
    }

and the simple enum:

    public enum myEnum 
    {
       Zero = 0,
       One = 1,
       Two = 2
    }
like image 500
Joshua Dixon Avatar asked Jan 22 '14 23:01

Joshua Dixon


1 Answers

The fact that 123 can get assigned to an enum that does not contain a value for 123 is not entirely the fault of Json.Net. It turns out that the C# runtime itself allows this assignment. You can see this for yourself if you run this small demonstration program:

class Program
{
    static void Main(string[] args)
    {
        // Direct cast from integer -- no error here
        MyEnum x = (MyEnum)123;
        Console.WriteLine(x);

        // Parsing a numeric string -- no error here either
        MyEnum y = (MyEnum)Enum.Parse(typeof(MyEnum), "456");
        Console.WriteLine(y);
    }

    public enum MyEnum
    {
        Zero = 0,
        One = 1,
        Two = 2
    }
}

So what is likely going on is that Json.Net is simply using Enum.Parse behind the scenes. I do not know why you are not getting an exception on your first case, however. When I try that, it fails (as I would expect).

In any case, if you require strict validation of possibly bad enum values, you can create a custom JsonConverter that will force the value to be valid (or optionally throw an exception). Here is a converter that should work for any kind of enum. (The code could probably be improved upon, but it works.)

class StrictEnumConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType.BaseType == typeof(Enum));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);

        try
        {
            // We're only interested in integers or strings;
            // all other token types should fall through
            if (token.Type == JTokenType.Integer ||
                token.Type == JTokenType.String)
            {
                // Get the string representation of the token
                // and check if it is numeric
                string s = token.ToString();
                int i;
                if (int.TryParse(s, out i))
                {
                    // If the token is numeric, try to find a matching
                    // name from the enum. If it works, convert it into
                    // the actual enum value; otherwise punt.
                    string name = Enum.GetName(objectType, i);
                    if (name != null)
                        return Enum.Parse(objectType, name);
                }
                else
                {
                    // We've got a non-numeric value, so try to parse it
                    // as is (case insensitive). If this doesn't work,
                    // it will throw an ArgumentException.
                    return Enum.Parse(objectType, s, true);
                }
            }
        }
        catch (ArgumentException)
        {
            // Eat the exception and fall through
        }

        // We got a bad value, so return the first value from the enum as
        // a default. Alternatively, you could throw an exception here to
        // halt the deserialization.
        IEnumerator en = Enum.GetValues(objectType).GetEnumerator();
        en.MoveNext();
        return en.Current;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString());
    }
}

Here is a demo using the converter on a hodgepodge of values:

class Program
{
    static void Main(string[] args)
    {
        // The first 12 values should deserialize to correct values;
        // the last 7 should all be forced to MyEnum.Zero.
        string json = @"
        {
            ""MyEnumList"":
            [
                ""Zero"", 
                ""One"", 
                ""Two"", 
                0,
                1,
                2,
                ""zero"",
                ""one"",
                ""two"",
                ""0"",
                ""1"",
                ""2"",
                ""BAD"", 
                ""123"", 
                123, 
                1.0,
                null,
                false,
                true
            ]
        }";

        MyClass obj = JsonConvert.DeserializeObject<MyClass>(json, 
                                                   new StrictEnumConverter());
        foreach (MyEnum e in obj.MyEnumList)
        {
            Console.WriteLine(e.ToString());
        }
    }

    public enum MyEnum
    {
        Zero = 0,
        One = 1,
        Two = 2
    }

    public class MyClass
    {
        public List<MyEnum> MyEnumList { get; set; }
    }
}

And here is the output:

Zero
One
Two
Zero
One
Two
Zero
One
Two
Zero
One
Two
Zero
Zero
Zero
Zero
Zero
Zero
Zero

By the way, to use this converter with Web API, you'll need to add this code to your Application_Start() method in Global.asax.cs:

JsonSerializerSettings settings = GlobalConfiguration.Configuration.Formatters 
                                  .JsonFormatter.SerializerSettings;
settings.Converters.Add(new StrictEnumConverter());
like image 119
Brian Rogers Avatar answered Nov 13 '22 20:11

Brian Rogers