【问题标题】:Custom JSON serializer for optional property with System.Text.Json带有 System.Text.Json 的可选属性的自定义 JSON 序列化程序
【发布时间】:2020-12-04 16:23:28
【问题描述】:

我正在尝试实现一个 JSON 序列化机制来处理 null 和缺少的 JSON 值,以便能够在需要时执行部分更新(这样它就不会在缺少值时触及数据库中的字段,但当值显式设置为 null 时,它会清除它。

我创建了一个从 Roslyn 的 Optional<T> 类型复制的自定义结构:

public readonly struct Optional<T>
{
    public Optional(T value)
    {
        this.HasValue = true;
        this.Value = value;
    }

    public bool HasValue { get; }
    public T Value { get; }
    public static implicit operator Optional<T>(T value) => new Optional<T>(value);
    public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
}

现在我希望能够对 JSON 进行序列化/反序列化,以便在通过 Optional&lt;T&gt; 对象往返时保留 JSON 中的任何缺失字段:

public class CustomType
{
    [JsonPropertyName("foo")]
    public Optional<int?> Foo { get; set; }

    [JsonPropertyName("bar")]
    public Optional<int?> Bar { get; set; }

    [JsonPropertyName("baz")]
    public Optional<int?> Baz { get; set; }
}

然后:

var options = new JsonSerializerOptions();
options.Converters.Add(new OptionalConverter());

string json = @"{""foo"":0,""bar"":null}";
CustomType parsed = JsonSerializer.Deserialize<CustomType>(json, options);
string roundtrippedJson = JsonSerializer.Serialize(parsed, options);

// json and roundtrippedJson should be equivalent
Console.WriteLine("json:             " + json);
Console.WriteLine("roundtrippedJson: " + roundtrippedJson);

我开始了一个基于JsonConverterFactory 的实现,但如果可选的HasValuefalse,我似乎找不到在序列化过程中省略该属性的正确方法:

public class OptionalConverter : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        if (!typeToConvert.IsGenericType) { return false; }
        if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false; }
        return true;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        Type valueType = typeToConvert.GetGenericArguments()[0];

        return (JsonConverter)Activator.CreateInstance(
            type: typeof(OptionalConverterInner<>).MakeGenericType(new Type[] { valueType }),
            bindingAttr: BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            args: null,
            culture: null
        );
    }

    private class OptionalConverterInner<T> : JsonConverter<Optional<T>>
    {
        public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            T value = JsonSerializer.Deserialize<T>(ref reader, options);
            return new Optional<T>(value);
        }

        public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options)
        {
            // Does not work (produces invalid JSON).
            // Problem: the object's key has already been written in the JSON writer at this point.
            if (value.HasValue)
            {
                JsonSerializer.Serialize(writer, value.Value, options);
            }
        }
    }
}

问题:这会产生以下无效的输出:

json:             {"foo":0,"bar":null}
roundtrippedJson: {"foo":0,"bar":null,"baz":}

我该如何解决这个问题?

【问题讨论】:

标签: c# json .net-core system.text.json


【解决方案1】:

自定义JsonConverter&lt;T&gt; 无法阻止转换器应用的值的序列化,请参阅[System.Text.Json] Converter-level conditional serialization #36275 进行确认。

在 .Net 5 中有一个选项可以忽略默认值,这应该可以满足您的需求,请参阅 How to ignore properties with System.Text.Json。本版本介绍JsonIgnoreCondition.WhenWritingDefault

public enum JsonIgnoreCondition
{
    // Property is never ignored during serialization or deserialization.
    Never = 0,
    // Property is always ignored during serialization and deserialization.
    Always = 1,
    // If the value is the default, the property is ignored during serialization.
    // This is applied to both reference and value-type properties and fields.
    WhenWritingDefault = 2,
    // If the value is null, the property is ignored during serialization.
    // This is applied only to reference-type properties and fields.
    WhenWritingNull = 3,
}

您将能够通过[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 或通过设置JsonSerializerOptions.DefaultIgnoreCondition 将条件全局应用于特定属性。

因此,在 .Net 5 中,您的课程将如下所示:

public class CustomType
{
    [JsonPropertyName("foo")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public Optional<int?> Foo { get; set; }

    [JsonPropertyName("bar")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public Optional<int?> Bar { get; set; }

    [JsonPropertyName("baz")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public Optional<int?> Baz { get; set; }
}

并且应该从OptionalConverterInner&lt;T&gt;.Write() 中删除HasValue 检查:

public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) =>
    JsonSerializer.Serialize(writer, value.Value, options);

演示小提琴 #1 here.

在 .Net 3 中,由于 System.Text.Json 中没有条件序列化机制,因此您唯一可以有条件地忽略没有值的可选属性的选择是为所有人编写 custom JsonConverter&lt;T&gt; 包含可选属性的类JsonSerializer does not provide any access to its internal contract information 这样的事实并不容易做到这一点,因此我们需要为每种此类类型手动制作转换器,或者通过反射编写我们自己的通用代码。

这是创建此类通用代码的一种尝试:

public interface IHasValue
{
    bool HasValue { get; }
    object GetValue();
}

public readonly struct Optional<T> : IHasValue
{
    public Optional(T value)
    {
        this.HasValue = true;
        this.Value = value;
    }

    public bool HasValue { get; }
    public T Value { get; }
    public object GetValue() => Value;
    public static implicit operator Optional<T>(T value) => new Optional<T>(value);
    public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
}

public class TypeWithOptionalsConverter<T> : JsonConverter<T> where T : class, new()
{
    class TypeWithOptionalsConverterContractFactory : JsonObjectContractFactory<T>
    {
        protected override Expression CreateSetterCastExpression(Expression e, Type t)
        {
            // (Optional<Nullable<T>>)(object)default(T) does not work, even though (Optional<Nullable<T>>)default(T) does work.
            // To avoid the problem we need to first cast to Nullable<T>, then to Optional<Nullable<T>>
            if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Optional<>))
                return Expression.Convert(Expression.Convert(e, t.GetGenericArguments()[0]), t);
            return base.CreateSetterCastExpression(e, t);
        }
    }
    
    static readonly TypeWithOptionalsConverterContractFactory contractFactory = new TypeWithOptionalsConverterContractFactory();
    
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var properties = contractFactory.GetProperties(typeToConvert);

        if (reader.TokenType == JsonTokenType.Null)
            return null;
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();
        var value = new T();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                return value;
            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();
            string propertyName = reader.GetString();
            if (!properties.TryGetValue(propertyName, out var property) || property.SetValue == null)
            {
                reader.Skip();
            }
            else
            {
                var type = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>) 
                    ? property.PropertyType.GetGenericArguments()[0] : property.PropertyType;
                var item = JsonSerializer.Deserialize(ref reader, type, options);
                property.SetValue(value, item);
            }
        }
        throw new JsonException();
    }           

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        foreach (var property in contractFactory.GetProperties(value.GetType()))
        {
            if (options.IgnoreReadOnlyProperties && property.Value.SetValue == null)
                continue;
            var item = property.Value.GetValue(value);
            if (item is IHasValue hasValue)
            {
                if (!hasValue.HasValue)
                    continue;
                writer.WritePropertyName(property.Key);
                JsonSerializer.Serialize(writer, hasValue.GetValue(), options);
            }
            else
            {
                if (options.IgnoreNullValues && item == null)
                    continue;
                writer.WritePropertyName(property.Key);
                JsonSerializer.Serialize(writer, item, property.Value.PropertyType, options);
            }
        }
        writer.WriteEndObject();
    }
}

public class JsonPropertyContract<TBase>
{
    internal JsonPropertyContract(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression)
    {
        this.GetValue = ExpressionExtensions.GetPropertyFunc<TBase>(property).Compile();
        if (property.GetSetMethod() != null)
            this.SetValue = ExpressionExtensions.SetPropertyFunc<TBase>(property, setterCastExpression).Compile();
        this.PropertyType = property.PropertyType;
    }
    public Func<TBase, object> GetValue { get; }
    public Action<TBase, object> SetValue { get; }
    public Type PropertyType { get; }
}

public class JsonObjectContractFactory<TBase>
{
    protected virtual Expression CreateSetterCastExpression(Expression e, Type t) => Expression.Convert(e, t);
    
    ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>> Properties { get; } = 
        new ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>>();

    ReadOnlyDictionary<string, JsonPropertyContract<TBase>> CreateProperties(Type type)
    {
        if (!typeof(TBase).IsAssignableFrom(type))
            throw new ArgumentException();
        var dictionary = type
            .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
            .Where(p => p.GetIndexParameters().Length == 0 && p.GetGetMethod() != null
                   && !Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)))
            .ToDictionary(p => p.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>()?.Name ?? p.Name,
                          p => new JsonPropertyContract<TBase>(p, (e, t) => CreateSetterCastExpression(e, t)), 
                          StringComparer.OrdinalIgnoreCase);
        return dictionary.ToReadOnly();
    }

    public IReadOnlyDictionary<string, JsonPropertyContract<TBase>> GetProperties(Type type) => Properties.GetOrAdd(type, t => CreateProperties(t));
}

public static class DictionaryExtensions
{
    public static ReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) => 
        new ReadOnlyDictionary<TKey, TValue>(dictionary ?? throw new ArgumentNullException());
}

public static class ExpressionExtensions
{
    public static Expression<Func<T, object>> GetPropertyFunc<T>(PropertyInfo property)
    {
        // (x) => (object)x.Property;
        var arg = Expression.Parameter(typeof(T), "x");
        var getter = Expression.Property(arg, property);
        var cast = Expression.Convert(getter, typeof(object));
        return Expression.Lambda<Func<T, object>>(cast, arg);
    }   

    public static Expression<Action<T, object>> SetPropertyFunc<T>(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression)
    {
        //(x, y) => x.Property = (TProperty)y       
        var arg1 = Expression.Parameter(typeof(T), "x");
        var arg2 = Expression.Parameter(typeof(object), "y");
        var cast = setterCastExpression(arg2, property.PropertyType);
        var setter = Expression.Call(arg1, property.GetSetMethod(), cast);
        return Expression.Lambda<Action<T, object>>(setter, arg1, arg2);
    }   
}

注意事项:

  • CustomType 仍然如您的问题所示。

  • 未尝试处理 JsonSerializerOptions.PropertyNamingPolicy 中存在的命名策略。如有必要,您可以在TypeWithOptionalsConverter&lt;T&gt; 中实现此功能。

  • 我添加了一个非通用接口IHasValue,以便在序列化过程中更轻松地访问装箱的Optional&lt;T&gt;

演示小提琴#2 here.

或者,您可以坚持使用在属性和联系人级别支持此功能的 Json.NET。见:

【讨论】:

    猜你喜欢
    • 2018-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多