45

How can I get Json.net not to throw up when my enum doesn't match string value provided in the json property?

This happens when I create enum based on current documentation, but the third party API adds more enum values later.

I would be happy with either marking special value as Unknown or using a nullable enum and unmatched value would return null.

Kugel
  • 19,354
  • 16
  • 71
  • 103

6 Answers6

64

You can solve this problem with a custom JsonConverter. Here is one I put together using a few pieces from the StringEnumConverter class that comes from Json.Net. It should give you the flexibility to handle things whatever way you decide. Here's how it works:

  • If the value found in the JSON matches the enum (either as a string or an integer), that value is used. (If the value is integer and there are multiple possible matches, the first of those is used.)
  • Otherwise if the enum type is nullable, then the value is set to null.
  • Otherwise if the enum has a value called "Unknown", then that value is used.
  • Otherwise the first value of the enum is used.

Here is the code. Feel free to change it to meet your needs.

class TolerantEnumConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        Type type = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType;
        return type.IsEnum;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        bool isNullable = IsNullableType(objectType);
        Type enumType = isNullable ? Nullable.GetUnderlyingType(objectType) : objectType;

        string[] names = Enum.GetNames(enumType);

        if (reader.TokenType == JsonToken.String)
        {
            string enumText = reader.Value.ToString();

            if (!string.IsNullOrEmpty(enumText))
            {
                string match = names
                    .Where(n => string.Equals(n, enumText, StringComparison.OrdinalIgnoreCase))
                    .FirstOrDefault();

                if (match != null)
                {
                    return Enum.Parse(enumType, match);
                }
            }
        }
        else if (reader.TokenType == JsonToken.Integer)
        {
            int enumVal = Convert.ToInt32(reader.Value);
            int[] values = (int[])Enum.GetValues(enumType);
            if (values.Contains(enumVal))
            {
                return Enum.Parse(enumType, enumVal.ToString());
            }
        }

        if (!isNullable)
        {
            string defaultName = names
                .Where(n => string.Equals(n, "Unknown", StringComparison.OrdinalIgnoreCase))
                .FirstOrDefault();

            if (defaultName == null)
            {
                defaultName = names.First();
            }

            return Enum.Parse(enumType, defaultName);
        }

        return null;
    }

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

    private bool IsNullableType(Type t)
    {
        return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>));
    }
}

Here is a demo which puts it the converter through its paces using a couple of different enums (one has an "Unknown" value, and the other does not):

[JsonConverter(typeof(TolerantEnumConverter))]
enum Status
{
    Ready = 1,
    Set = 2,
    Go = 3
}

[JsonConverter(typeof(TolerantEnumConverter))]
enum Color
{
    Red = 1,
    Yellow = 2,
    Green = 3,
    Unknown = 99
}

class Foo
{
    public Status NonNullableStatusWithValidStringValue { get; set; }
    public Status NonNullableStatusWithValidIntValue { get; set; }
    public Status NonNullableStatusWithInvalidStringValue { get; set; }
    public Status NonNullableStatusWithInvalidIntValue { get; set; }
    public Status NonNullableStatusWithNullValue { get; set; }

    public Status? NullableStatusWithValidStringValue { get; set; }
    public Status? NullableStatusWithValidIntValue { get; set; }
    public Status? NullableStatusWithInvalidStringValue { get; set; }
    public Status? NullableStatusWithInvalidIntValue { get; set; }
    public Status? NullableStatusWithNullValue { get; set; }

    public Color NonNullableColorWithValidStringValue { get; set; }
    public Color NonNullableColorWithValidIntValue { get; set; }
    public Color NonNullableColorWithInvalidStringValue { get; set; }
    public Color NonNullableColorWithInvalidIntValue { get; set; }
    public Color NonNullableColorWithNullValue { get; set; }

    public Color? NullableColorWithValidStringValue { get; set; }
    public Color? NullableColorWithValidIntValue { get; set; }
    public Color? NullableColorWithInvalidStringValue { get; set; }
    public Color? NullableColorWithInvalidIntValue { get; set; }
    public Color? NullableColorWithNullValue { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""NonNullableStatusWithValidStringValue"" : ""Set"",
            ""NonNullableStatusWithValidIntValue"" : 2,
            ""NonNullableStatusWithInvalidStringValue"" : ""Blah"",
            ""NonNullableStatusWithInvalidIntValue"" : 9,
            ""NonNullableStatusWithNullValue"" : null,
            ""NullableStatusWithValidStringValue"" : ""Go"",
            ""NullableStatusWithValidIntValue"" : 3,
            ""NullableStatusWithNullValue"" : null,
            ""NullableStatusWithInvalidStringValue"" : ""Blah"",
            ""NullableStatusWithInvalidIntValue"" : 9,
            ""NonNullableColorWithValidStringValue"" : ""Green"",
            ""NonNullableColorWithValidIntValue"" : 3,
            ""NonNullableColorWithInvalidStringValue"" : ""Blah"",
            ""NonNullableColorWithInvalidIntValue"" : 0,
            ""NonNullableColorWithNullValue"" : null,
            ""NullableColorWithValidStringValue"" : ""Yellow"",
            ""NullableColorWithValidIntValue"" : 2,
            ""NullableColorWithNullValue"" : null,
            ""NullableColorWithInvalidStringValue"" : ""Blah"",
            ""NullableColorWithInvalidIntValue"" : 0,
        }";

        Foo foo = JsonConvert.DeserializeObject<Foo>(json);
        foreach (PropertyInfo prop in typeof(Foo).GetProperties())
        {
            object val = prop.GetValue(foo, null);
            Console.WriteLine(prop.Name + ": " + 
                             (val == null ? "(null)" : val.ToString()));
        }
    }
}

Output:

NonNullableStatusWithValidStringValue: Set
NonNullableStatusWithValidIntValue: Set
NonNullableStatusWithInvalidStringValue: Ready
NonNullableStatusWithInvalidIntValue: Ready
NonNullableStatusWithNullValue: Ready
NullableStatusWithValidStringValue: Go
NullableStatusWithValidIntValue: Go
NullableStatusWithInvalidStringValue: (null)
NullableStatusWithInvalidIntValue: (null)
NullableStatusWithNullValue: (null)
NonNullableColorWithValidStringValue: Green
NonNullableColorWithValidIntValue: Green
NonNullableColorWithInvalidStringValue: Unknown
NonNullableColorWithInvalidIntValue: Unknown
NonNullableColorWithNullValue: Unknown
NullableColorWithValidStringValue: Yellow
NullableColorWithValidIntValue: Yellow
NullableColorWithInvalidStringValue: (null)
NullableColorWithInvalidIntValue: (null)
NullableColorWithNullValue: (null)
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • 2
    I thought there would be something built-in. Sounded like a common case. – Kugel Mar 31 '14 at 22:29
  • 2
    There is a `StringEnumConverter` that ships with Json.Net, but it throws an exception if the string value is not found in the enum. I did check this before I went for the custom converter. If you want something more lax, it seems you have to write your own. – Brian Rogers Mar 31 '14 at 22:32
  • Looks good, the only enhancement I can think of -- adding support for `EnumMember` attribute. – Eugene D. Gubenkov Apr 02 '17 at 06:34
  • 2
    I've extended this version with adding `EnumMember` support, it's available here: https://gist.github.com/gubenkoved/999eb73e227b7063a67a50401578c3a7 – Eugene D. Gubenkov Apr 02 '17 at 07:15
36

Looking through the handful of suggestions that exist for this problem, all of them use StringEnumConverter as a backbone, but no suggestions use it through inheritance. If your scenario was like mine, I was taking a 3rd party API response, which has ton of possible enum values, that may change over time. I only care about maybe 10 of those values, so all the other values I want to fallback on a default value(like Unknown). Here's my enum converter to do this:

/// <inheritdoc />
/// <summary>
/// Defaults enum values to the base value if 
/// </summary>
public class DefaultUnknownEnumConverter : StringEnumConverter
{
    /// <summary>
    /// The default value used to fallback on when a enum is not convertable.
    /// </summary>
    private readonly int defaultValue;

    /// <inheritdoc />
    /// <summary>
    /// Default constructor. Defaults the default value to 0.
    /// </summary>
    public DefaultUnknownEnumConverter() 
    {}

    /// <inheritdoc />
    /// <summary>
    /// Sets the default value for the enum value.
    /// </summary>
    /// <param name="defaultValue">The default value to use.</param>
    public DefaultUnknownEnumConverter(int defaultValue)
    {
        this.defaultValue = defaultValue;
    }

    /// <inheritdoc />
    /// <summary>
    /// Reads the provided JSON and attempts to convert using StringEnumConverter. If that fails set the value to the default value.
    /// </summary>
    /// <param name="reader">Reads the JSON value.</param>
    /// <param name="objectType">Current type that is being converted.</param>
    /// <param name="existingValue">The existing value being read.</param>
    /// <param name="serializer">Instance of the JSON Serializer.</param>
    /// <returns>The deserialized value of the enum if it exists or the default value if it does not.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return base.ReadJson(reader, objectType, existingValue, serializer);
        }
        catch
        {
            return Enum.Parse(objectType, $"{defaultValue}");
        }
    }

    /// <inheritdoc />
    /// <summary>
    /// Validates that this converter can handle the type that is being provided.
    /// </summary>
    /// <param name="objectType">The type of the object being converted.</param>
    /// <returns>True if the base class says so, and if the value is an enum and has a default value to fall on.</returns>
    public override bool CanConvert(Type objectType)
    {
        return base.CanConvert(objectType) && objectType.GetTypeInfo().IsEnum && Enum.IsDefined(objectType, defaultValue);
    }
}

Usage is the same as other examples:

[JsonConverter(typeof(DefaultUnknownEnumConverter))]
public enum Colors
{
    Unknown,
    Red,
    Blue,
    Green,
}

[JsonConverter(typeof(DefaultUnknownEnumConverter), (int) NotFound)]
public enum Colors
{        
    Red = 0,
    Blue,
    Green,
    NotFound
}
Tyler Robinson
  • 361
  • 3
  • 2
  • 3
    A small Addition. You can also add the following lines to the catch block to handle deserialization of nullable enums: var underlyingType = Nullable.GetUnderlyingType(objectType); if (underlyingType != null) return Enum.Parse(underlyingType , $"{defaultValue}") return Enum.Parse(objectType, $"{defaultValue}"); – Nikolay Jan 31 '19 at 06:33
13

If you only care about deserialization, another simple thing you could do is to define the enum field as string and add another 'get' only field that parses the string field to either one of the known values or to 'unknown'. This field should be 'JsonIgnore'd.

Vignesh Chandramohan
  • 1,306
  • 10
  • 15
8

You could use a custom StringEnumConverter, like this:

public class SafeStringEnumConverter : StringEnumConverter
{
    public object DefaultValue { get; }

    public SafeStringEnumConverter(object defaultValue)
    {
        DefaultValue = defaultValue;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return base.ReadJson(reader, objectType, existingValue, serializer);
        }
        catch
        {      
            return DefaultValue;
        }
    }
}

Then you can use it as follows:

[JsonConverter(typeof(SafeStringEnumConverter), Unknown)]
public enum Colors
{
    Unknown,

    [EnumMember(Value = "MY_VALUE_1")]
    MyValue,

    [EnumMember(Value = "MY_VALUE_2")]
    MyValue2
}
mfabruno
  • 131
  • 3
  • 12
Diego Torres
  • 1,213
  • 12
  • 5
2

Here's some sample code for Vignesh Chandramohan answer. Certainly the simplest solution if you're just deserialising.

public class SampleClass
{
    [JsonProperty("sampleEnum")] public string sampleEnumString;

    [JsonIgnore]
    public SampleEnum sampleEnum
    {
        get
        {
            if (Enum.TryParse<SampleEnum>(sampleEnumString, true, out var result))
            {
                return result;
            }

            return SampleEnum.UNKNOWN;
        }
    }
}

public enum SampleEnum
{
    UNKNOWN,
    V1,
    V2,
    V3
}
Will Calderwood
  • 4,393
  • 3
  • 39
  • 64
  • FYI, you can make the `sampleEnumString` member `private` if you don't want to expose it in the public interface of `SampleClass`. It will still work because `[JsonProperty]` allows the deserializer to see it. – Brian Rogers Oct 30 '19 at 14:51
0

Improving on @BrianRogers I have wrote the following code and it passes all of his tests + it deals with the EnumAttribute questions! (I had the same problem of Nullables Enums recently)

class TolerantEnumConverter : StringEnumConverter
{

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return base.ReadJson(reader, objectType, existingValue, serializer);
        }
        catch
        {
            if (IsNullableType(objectType))
                return null;

            //I would throw the exception, but to pass the tests 
            return Enum.Parse(objectType, Enum.GetNames(objectType).First());
        }
    }

    private static bool IsNullableType(Type t)
    {
        if (t == null)
            throw new ArgumentNullException(nameof(t));

        return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>));
    }
}
mfabruno
  • 131
  • 3
  • 12