【发布时间】:2014-03-31 01:03:39
【问题描述】:
当我的枚举与 json 属性中提供的字符串值不匹配时,如何让 Json.net 不出错?
当我根据当前文档创建枚举时会发生这种情况,但第三方 API 稍后会添加更多枚举值。
我很乐意将特殊值标记为未知或使用可为空的枚举,不匹配的值将返回空值。
【问题讨论】:
当我的枚举与 json 属性中提供的字符串值不匹配时,如何让 Json.net 不出错?
当我根据当前文档创建枚举时会发生这种情况,但第三方 API 稍后会添加更多枚举值。
我很乐意将特殊值标记为未知或使用可为空的枚举,不匹配的值将返回空值。
【问题讨论】:
您可以使用自定义JsonConverter 解决此问题。这是我使用来自 Json.Net 的 StringEnumConverter 类中的几部分组合而成的。它应该使您可以灵活地处理任何您决定的事情。以下是它的工作原理:
这里是代码。随意更改它以满足您的需求。
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<>));
}
}
这是一个演示,它使用几个不同的枚举(一个具有“未知”值,而另一个没有)将转换器通过其步伐:
[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()));
}
}
}
输出:
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)
【讨论】:
StringEnumConverter,但如果在枚举中找不到字符串值,它会抛出异常。在我选择自定义转换器之前,我确实检查了这一点。如果你想要更宽松的东西,看来你必须自己写。
EnumMember属性的支持。
EnumMember 支持,它可以在这里找到:gist.github.com/gubenkoved/999eb73e227b7063a67a50401578c3a7
查看针对此问题存在的少数建议,它们都使用 StringEnumConverter 作为主干,但没有建议通过继承使用它。如果你的场景和我的一样,我正在接受第 3 方 API 响应,它有大量可能的枚举值,这些值可能会随着时间而改变。我只关心这些值中的 10 个,所以我想使用默认值(如未知)的所有其他值。这是我的枚举转换器:
/// <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);
}
}
用法同其他例子:
[JsonConverter(typeof(DefaultUnknownEnumConverter))]
public enum Colors
{
Unknown,
Red,
Blue,
Green,
}
[JsonConverter(typeof(DefaultUnknownEnumConverter), (int) NotFound)]
public enum Colors
{
Red = 0,
Blue,
Green,
NotFound
}
【讨论】:
如果您只关心反序列化,您可以做的另一件简单的事情是将枚举字段定义为字符串,并添加另一个仅“获取”字段,将字符串字段解析为已知值或“未知”。该字段应为“JsonIgnore”。
【讨论】:
您可以使用自定义的 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;
}
}
}
那么你可以如下使用它:
[JsonConverter(typeof(SafeStringEnumConverter), Unknown)]
public enum Colors
{
Unknown,
[EnumMember(Value = "MY_VALUE_1")]
MyValue,
[EnumMember(Value = "MY_VALUE_2")]
MyValue2
}
【讨论】:
这是Vignesh Chandramohan 答案的一些示例代码。如果您只是反序列化,当然是最简单的解决方案。
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
}
【讨论】:
SampleClass 的公共界面中公开sampleEnumString 成员private。它仍然可以工作,因为[JsonProperty] 允许反序列化器看到它。
改进@BrianRogers 我编写了以下代码,它通过了他的所有测试 + 它处理了 EnumAttribute 问题! (我最近遇到了同样的 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<>));
}
}
【讨论】: