英文:
System.Text.Json JsonStringEnumConverter with custom fallback in case of deserialization failures
问题
我有一个.NET 6程序,需要将JSON字符串值(由外部API返回)反序列化为.NET枚举。
问题在于有100多个可能的枚举值(而且可能会在我不知情的情况下添加更多),但我只对其中的一些感兴趣。因此,我想定义一个枚举类型,如下所示,并将我不感兴趣的所有值反序列化为`MyEnum.Unknown`:
public enum MyEnum
{
Unknown = 0,
Value1,
Value2,
Value3,
// 只有我感兴趣的值将被定义
}
如果我使用Newtonsoft.Json,我可以通过一个自定义的JSON转换器轻松实现:
public class DefaultFallbackStringEnumConverter : StringEnumConverter
{
private readonly object _defaultValue;
public DefaultFallbackStringEnumConverter() : this(0)
{
}
public DefaultFallbackStringEnumConverter(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 (JsonException)
{
return _defaultValue;
}
}
}
但是使用System.Text.Json,我无法弄清楚如何轻松地做到这一点,因为STJ中的[`JsonStringEnumConverter`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter?view=net-6.0)实际上是一个`JsonConverterFactory`,它本身不执行序列化(正如您可以在[这里](https://github.com/dotnet/runtime/blob/a08d9ce2caf02455c0b825bcdc32974bdf769a80/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs#L77-L107)看到的,它在所有`JsonConverter`的覆盖中抛出异常)。相反,该工厂创建实际执行工作的[`EnumConverter<T>`](https://github.com/dotnet/runtime/blob/a08d9ce2caf02455c0b825bcdc32974bdf769a80/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs),但是`EnumConverter<T>`是内部的,所以我甚至不能在用户代码中引用或继承它。
你知道如何在STJ中轻松实现这一点吗,或者根本不可能吗?非常感谢帮助!
英文:
I have a .NET 6 program that needs to deserialize a JSON string value (returned by external API) into a .NET enum.
The issue is that there are over 100 possible enum values (and more could be added without my knowledge), but I'm only interested in a few of them. So I would like to define an enum type like this and deserialize all the values that I'm not interested in to MyEnum.Unknown
:
public enum MyEnum
{
Unknown = 0,
Value1,
Value2,
Value3,
// only values that I'm interested in will be defined
}
If I'm using Newtonsoft.Json, I can do this quite easily with a custom JSON converter:
public class DefaultFallbackStringEnumConverter : StringEnumConverter
{
private readonly object _defaultValue;
public DefaultFallbackStringEnumConverter() : this(0)
{
}
public DefaultFallbackStringEnumConverter(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 (JsonException)
{
return _defaultValue;
}
}
}
But with System.Text.Json, I can't figure out how this can be done easily, because the JsonStringEnumConverter
in STJ is actually a JsonConverterFactory
that doesn't do the serialization itself (it throws exceptions in all overrides of JsonConverter
as you can see here). Instead the factory creates EnumConverter<T>
s that actually do the work, but EnumConverter<T>
is internal so I can't even reference or inherit from it in user code.
Any idea how this can be done easily with STJ or is it not possible at all? Thanks a lot for the help!
答案1
得分: 2
你可以创建一个自定义的 JsonConverter
public class MyEnumJsonConverter : JsonConverter<MyEnum>
{
public override MyEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return Enum.TryParse<MyEnum>(reader.GetString(), true, out var val) ? val : default;
}
public override void Write(Utf8JsonWriter writer, MyEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
然后,在你想要使用这个转换器的属性上添加属性
public class Test
{
[JsonConverter(typeof(MyEnumJsonConverter))]
public MyEnum MyEnum { get; set; }
}
英文:
You can create a custom JsonConverter
public class MyEnumJsonConverter : JsonConverter<MyEnum>
{
public override MyEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return Enum.TryParse<MyEnum>(reader.GetString(),true, out var val) ? val : default;
}
public override void Write(Utf8JsonWriter writer, MyEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
Then add an attribute on the property you want the converter to kick in
public class Test
{
[JsonConverter(typeof(MyEnumJsonConverter))]
public MyEnum MyEnum { get; set; }
}
答案2
得分: 2
你可以使用装饰器模式来包装JsonStringEnumConverter
工厂,将其包装在一个装饰器中,该装饰器的CreateConverter()
方法包装返回的EnumConverter<T>
在一个内部装饰器中,捕获异常并返回默认值。
以下是示例代码:
public class DefaultFallbackStringEnumConverter : JsonConverterFactoryDecorator
{
public DefaultFallbackStringEnumConverter(JsonStringEnumConverter inner) : base(inner) { }
public DefaultFallbackStringEnumConverter() : this(new JsonStringEnumConverter()) { }
protected virtual T GetDefaultValue<T>() where T : struct, Enum => default(T);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var inner = base.CreateConverter(typeToConvert, options);
return (JsonConverter?)Activator.CreateInstance(typeof(EnumConverterDecorator<>).MakeGenericType(Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert), new object?[] { this, inner });
}
sealed class EnumConverterDecorator<T> : JsonConverter<T> where T : struct, Enum
{
readonly DefaultFallbackStringEnumConverter parent;
readonly JsonConverter<T> inner;
public EnumConverterDecorator(DefaultFallbackStringEnumConverter parent, JsonConverter inner) =>
(this.parent, this.inner) = (parent ?? throw new ArgumentException(nameof(parent)), (inner as JsonConverter<T>) ?? throw new ArgumentException(nameof(inner)));
public override bool CanConvert(Type typeToConvert) => inner.CanConvert(typeToConvert);
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
try
{
return inner.Read(ref reader, typeToConvert, options);
}
catch (JsonException)
{
return parent.GetDefaultValue<T>();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => inner.Write(writer, value, options);
}
}
public class JsonConverterFactoryDecorator : JsonConverterFactory
{
readonly JsonConverterFactory inner;
public JsonConverterFactoryDecorator(JsonConverterFactory inner) => this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
public override bool CanConvert(Type typeToConvert) => inner.CanConvert(typeToConvert);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => inner.CreateConverter(typeToConvert, options);
}
请注意,与Json.NET不同,System.Text.Json没有与Json.NET的ConverterParameters
等效的功能(请参阅问题 #54187以确认),因此,如果需要为特定的枚举设置不同的默认值,您需要为该特定枚举子类化DefaultFallbackStringEnumConverter
,例如:
public enum MyEnum2
{
Unknown1 = 1, // Use this value for unknown values
Value2 = 2,
Value3 = 3,
}
public class MyEnum2Converter : DefaultFallbackStringEnumConverter
{
protected override T GetDefaultValue<T>() => typeof(T) == typeof(MyEnum2) ? (T)(object)MyEnum2.Unknown1 : base.GetDefaultValue<T>();
public override bool CanConvert(Type typeToConvert) => base.CanConvert(typeToConvert) && typeToConvert == typeof(MyEnum2);
}
然后,如果您的模型如下所示:
public record Model(MyEnum MyEnum,
[property: JsonConverter(typeof(MyEnum2Converter))] MyEnum2? MyEnum2);
您的JSON如下所示:
{"MyEnum": "missing value", "MyEnum2": "missing value" }
您将能够进行反序列化和重新序列化,如下所示:
var options = new JsonSerializerOptions
{
Converters = { new DefaultFallbackStringEnumConverter() },
};
var model = JsonSerializer.Deserialize<Model>(json, options);
var newJson = JsonSerializer.Serialize(model, options);
演示示例可以在此处找到。
英文:
You could use the decorator pattern and wrap the JsonStringEnumConverter
factory in a decorator whose CreateConverter()
method wraps the returned EnumConverter<T>
in some inner decorator that catches the exception and returns a default value.
The following does that:
public class DefaultFallbackStringEnumConverter : JsonConverterFactoryDecorator
{
public DefaultFallbackStringEnumConverter(JsonStringEnumConverter inner) : base(inner) { }
public DefaultFallbackStringEnumConverter() : this(new JsonStringEnumConverter()) { }
protected virtual T GetDefaultValue<T>() where T : struct, Enum => default(T);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var inner = base.CreateConverter(typeToConvert, options);
return (JsonConverter?)Activator.CreateInstance(typeof(EnumConverterDecorator<>).MakeGenericType(Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert), new object? [] { this, inner });
}
sealed class EnumConverterDecorator<T> : JsonConverter<T> where T : struct, Enum
{
readonly DefaultFallbackStringEnumConverter parent;
readonly JsonConverter<T> inner;
public EnumConverterDecorator(DefaultFallbackStringEnumConverter parent, JsonConverter inner) =>
(this.parent, this.inner)= (parent ?? throw new ArgumentException(nameof(parent)), (inner as JsonConverter<T>) ?? throw new ArgumentException(nameof(inner)));
public override bool CanConvert(Type typeToConvert) => inner.CanConvert(typeToConvert);
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
try
{
return inner.Read(ref reader, typeToConvert, options);
}
catch (JsonException)
{
return parent.GetDefaultValue<T>();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => inner.Write(writer, value, options);
}
}
public class JsonConverterFactoryDecorator : JsonConverterFactory
{
readonly JsonConverterFactory inner;
public JsonConverterFactoryDecorator(JsonConverterFactory inner) => this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
public override bool CanConvert(Type typeToConvert) => inner.CanConvert(typeToConvert);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => inner.CreateConverter(typeToConvert, options);
}
Do note that, unlike Json.NET, System.Text.Json does not have an equivalent to Json.NET's ConverterParameters
(see issue #54187 for confirmation) so if you need a different default value for a specific enum, you will need to subclass DefaultFallbackStringEnumConverter
for that specific enum, e.g. like so:
public enum MyEnum2
{
Unknown1 = 1, // Use this value for unknown values
Value2 = 2,
Value3 = 3,
}
public class MyEnum2Converter : DefaultFallbackStringEnumConverter
{
protected override T GetDefaultValue<T>() => typeof(T) == typeof(MyEnum2) ? (T)(object)MyEnum2.Unknown1 : base.GetDefaultValue<T>();
public override bool CanConvert(Type typeToConvert) => base.CanConvert(typeToConvert) && typeToConvert == typeof(MyEnum2);
}
Then if your model looks like e.g.:
public record Model(MyEnum MyEnum,
[property: JsonConverter(typeof(MyEnum2Converter))] MyEnum2? MyEnum2);
And your JSON looks like:
{"MyEnum" : "missing value", "MyEnum2" : "missing value" }
You will be able to deserialize and re-serialize as follows:
var options = new JsonSerializerOptions
{
Converters = { new DefaultFallbackStringEnumConverter() },
};
var model = JsonSerializer.Deserialize<Model>(json, options);
var newJson = JsonSerializer.Serialize(model, options);
Demo fiddle here.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论