48

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.

8 Answers 8

69

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) 
Sign up to request clarification or add additional context in comments.

4 Comments

I thought there would be something built-in. Sounded like a common case.
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.
Looks good, the only enhancement I can think of -- adding support for EnumMember attribute.
I've extended this version with adding EnumMember support, it's available here: gist.github.com/gubenkoved/999eb73e227b7063a67a50401578c3a7
38

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 } 

1 Comment

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}");
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.

1 Comment

an example would be nice
12

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 } 

2 Comments

I like this solution a lot!
How is this answer any different from stackoverflow.com/a/51847437/70345 which was posted nearly 2 years before?
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 } 

1 Comment

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.
1

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<>)); } } 

Comments

1

If someone is looking for a similar solution for System.Text.Json, I wrote a generic converter that can be used for a specific enum type:

public class FallbackJsonEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum { public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Enum.TryParse(reader.GetString(), true, out TEnum result) ? result : default; public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); } 

Just decorade the property in your model and you will get the default value of the enum instead an exception if you try to deserialize an unknown value:

public record MyDto { [JsonConverter(typeof(FallbackJsonEnumConverter<MyEnum>))] public MyEnum EnumValue { get; init; } } 

Comments

0

Why not keep it simple?

There is no use for a nullable enum, since this can be simplified just by adding a default option, e.g. "Unknown".

And secondly, if you don't need the support for numerical values, the code can be simplified like this:

public class JsonEnumConverter : JsonConverter<object> { public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum; public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // try to convert Json string if (reader.TokenType == JsonTokenType.String) if (Enum.TryParse(typeToConvert, reader.GetString(), out var result)) return result; // else return the default enum value return Activator.CreateInstance(typeToConvert); } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } } 

Usage:

var data = JsonSerializer.Deserialize<TResult>(jsonString, new JsonSerializerOptions { Converters = { new JsonEnumConverter() } }); 

Just make sure your enum has a default option.

 public enum MyEnum { Unknown = 0, // this will be the default OneValidOption, AnotherOption } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.