System.Text.Json JsonStringEnumConverter with custom fallback in case of deserialization failures

huangapple go评论75阅读模式
英文:

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&lt;MyEnum&gt;
{
    public override MyEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
       return Enum.TryParse&lt;MyEnum&gt;(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&lt;T&gt;在一个内部装饰器中,捕获异常并返回默认值。

以下是示例代码:

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&lt;T&gt; 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&lt;T&gt;() where T : struct, Enum =&gt; default(T);

	public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
	{
		var inner = base.CreateConverter(typeToConvert, options);
		return (JsonConverter?)Activator.CreateInstance(typeof(EnumConverterDecorator&lt;&gt;).MakeGenericType(Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert), new object? [] { this, inner });
	}

	sealed class EnumConverterDecorator&lt;T&gt; : JsonConverter&lt;T&gt; where T : struct, Enum
	{
		readonly DefaultFallbackStringEnumConverter parent;
		readonly JsonConverter&lt;T&gt; inner;
		public EnumConverterDecorator(DefaultFallbackStringEnumConverter parent, JsonConverter inner) =&gt; 
			(this.parent, this.inner)= (parent ?? throw new ArgumentException(nameof(parent)), (inner as JsonConverter&lt;T&gt;) ?? throw new ArgumentException(nameof(inner)));

		public override bool CanConvert(Type typeToConvert) =&gt; 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&lt;T&gt;();
			}
		}
		public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =&gt; inner.Write(writer, value, options);
	}
}

public class JsonConverterFactoryDecorator : JsonConverterFactory
{
	readonly JsonConverterFactory inner;
	public JsonConverterFactoryDecorator(JsonConverterFactory inner) =&gt; this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
	public override bool CanConvert(Type typeToConvert) =&gt; inner.CanConvert(typeToConvert);
	public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) =&gt; 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&lt;T&gt;() =&gt; typeof(T) == typeof(MyEnum2) ? (T)(object)MyEnum2.Unknown1 : base.GetDefaultValue&lt;T&gt;();
	public override bool CanConvert(Type typeToConvert) =&gt; base.CanConvert(typeToConvert) &amp;&amp; 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:

{&quot;MyEnum&quot; : &quot;missing value&quot;, &quot;MyEnum2&quot; : &quot;missing value&quot; }

You will be able to deserialize and re-serialize as follows:

var options = new JsonSerializerOptions
{
	Converters = { new DefaultFallbackStringEnumConverter() },
};

var model = JsonSerializer.Deserialize&lt;Model&gt;(json, options);

var newJson = JsonSerializer.Serialize(model, options);

Demo fiddle here.

huangapple
  • 本文由 发表于 2023年7月18日 11:00:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/76709269.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定