简体   繁体   English

如何在 json 反序列化期间忽略未知的枚举值?

[英]How can I ignore unknown enum values during json deserialization?

How can I get Json.net not to throw up when my enum doesn't match string value provided in the json property?当我的枚举与 json 属性中提供的字符串值不匹配时,如何让 Json.net 不抛出?

This happens when I create enum based on current documentation, but the third party API adds more enum values later.当我根据当前文档创建枚举时会发生这种情况,但第三方 API 稍后会添加更多枚举值。

I would be happy with either marking special value as Unknown or using a nullable enum and unmatched value would return null.我会很高兴将特殊值标记为未知或使用可为空的枚举和不匹配的值将返回空值。

You can solve this problem with a custom JsonConverter .您可以使用自定义JsonConverter解决此问题。 Here is one I put together using a few pieces from the StringEnumConverter class that comes from Json.Net.这是我使用来自StringEnumConverter类中的一些片段组合在一起的一个。 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.如果在 JSON 中找到的值与枚举匹配(作为字符串或整数),则使用该值。 (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)

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.查看针对此问题存在的少数建议,所有建议都使用 StringEnumConverter 作为主干,但没有建议通过继承使用它。 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.如果您的场景和我的一样,我采用了 3rd 方 API 响应,其中包含大量可能的枚举值,这些值可能会随着时间的推移而改变。 I only care about maybe 10 of those values, so all the other values I want to fallback on a default value(like Unknown).我只关心这些值中的 10 个,所以我想回退到默认值(如未知)的所有其他值。 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
}

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.此字段应为“JsonIgnore”。

You could use a custom StringEnumConverter, like this:您可以使用自定义 StringEnumConverter,如下所示:

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
}

Here's some sample code for Vignesh Chandramohan answer.这是Vignesh Chandramohan答案的一些示例代码。 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
}

Improving on @ BrianRogers I have wrote the following code and it passes all of his tests + it deals with the EnumAttribute questions!改进@ BrianRogers我编写了以下代码,它通过了他的所有测试+它处理 EnumAttribute 问题! (I had the same problem of Nullables Enums recently) (我最近遇到了 Nullables Enums 同样的问题)

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM