使用 System.Text.Json 自定义可选属性的 JSON 序列化器

2024-04-03

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

我创建了一个从 Roslyn 复制的自定义结构Optional<T> https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/Optional.cs type:

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 进行序列化/反序列化,以便在通过以下方式往返时保留 JSON 中任何缺失的字段:Optional<T> object:

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

Then:

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,但如果可选的,我似乎找不到在序列化期间省略该属性的正确方法HasValue is false:

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":}

我该如何解决这个问题?


一个习俗JsonConverter<T>无法阻止转换器应用的值的序列化,请参阅[System.Text.Json] 转换器级条件序列化 #36275 https://github.com/dotnet/runtime/issues/36275确认。

在.Net 5中有一个选项可以忽略默认值,它应该满足您的需要,请参阅如何使用 System.Text.Json 忽略属性 https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-ignore-properties?pivots=dotnet-5-0#ignore-individual-properties。该版本介绍了JsonIgnoreCondition.WhenWritingDefault https://github.com/dotnet/runtime/blob/master/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonIgnoreCondition.cs:

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)] https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonignoreattribute.condition?view=net-5.0或通过设置全局JsonSerializerOptions.DefaultIgnoreCondition https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.defaultignorecondition?view=net-5.0.

因此,在 .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; }
}

And the HasValue检查应该从OptionalConverterInner<T>.Write():

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

演示小提琴 #1here https://dotnetfiddle.net/tY8Hfx.

在.Net 3中,因为没有条件序列化机制System.Text.Json,有条件地省略没有值的可选属性的唯一选择是编写custom JsonConverter<T> https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to 对于包含可选属性的所有类。这并不容易,因为JsonSerializer 不提供对其内部合同信息的任何访问 https://github.com/dotnet/runtime/issues/34456因此,我们需要为每种类型手工制作一个转换器,或者通过反射编写我们自己的通用代码。

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

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

Notes:

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

  • 没有尝试处理命名策略的存在JsonSerializerOptions.PropertyNamingPolicy https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.propertynamingpolicy?view=netcore-3.1#System_Text_Json_JsonSerializerOptions_PropertyNamingPolicy。你可以在TypeWithOptionalsConverter<T>如果需要的话。

  • 我添加了一个非通用接口IHasValue以便更轻松地访问盒装Optional<T>在序列化期间。

演示小提琴#2here https://dotnetfiddle.net/MslUoq.

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

  • (可选)根据运行时值序列化属性 https://stackoverflow.com/q/12522000/3744182(本质上是你的问题的重复)。

  • 如何根据用户授权动态jsonignore? https://stackoverflow.com/q/16138840/3744182

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 System.Text.Json 自定义可选属性的 JSON 序列化器 的相关文章

随机推荐

  • 如何在React ChartJS中获取填充圆图?

    我想介绍一个填充圆图 气泡图的变体 仅包含半径作为其尺寸 React Chartjs乃至Chartjs仅支持气泡图 不支持圆形图 这是我想要添加的预期图表的示例 我可以使用这样的东西吗react chartjs 例如 需要执行如下操作来定期
  • Java Clip(声音/音频)使用 close() 关闭后内存泄漏

    以下代码创建一个新的音频剪辑 播放它 休眠 3 秒 然后在播放完毕后关闭它 尽管调用了 close 但每次运行 while 循环时 我都会看到 jvm 的内存使用量随着声音剪辑的大小而增加 我正在参与一个用java编码的游戏 并且正在处理声
  • jQuery 修剪不会删除?

    如何修剪字符串中的所有空格 即使它们是由不间断空格引起的 nbsp 例如 var foo trim p nbsp foo nbsp p text foo 的值为 foo 代替 foo UPDATE所以 问题不在于 jQuery 的修剪功能
  • 在redis队列中调用django自定义管理命令

    是否可以将 django 管理命令作为 redis 中的排队作业调用 这对我来说失败了 from django core management import call command from rq import Queue queue Q
  • 如何编写一个程序,将整数序列读入数组并计算数组中所有元素的交替和?

    编写一个程序 将整数序列读入数组 并计算数组中所有元素的交替和 例如 如果程序使用输入数据执行 1 4 9 16 9 7 4 9 11 然后它计算 1 4 9 16 9 7 4 9 11 2 到目前为止我有以下代码 import java
  • C++ 构造函数的模板特化

    我有一个模板类 A 和两个 typedef A 和 A 如何重写 A 的构造函数 以下不起作用 template
  • 无法将 GridView 绑定到 LINQ to SQL 结果

    好吧 我确实是 LINQ 的新手 上周我已经阅读了有关 LINQ 的所有内容 我只是在玩玩 试图遵循我找到的一些示例 事实上 来自 Scott Gu 的关于该主题的 PDF 但我完全不知所措 有人可以告诉我为什么当我使用下面的代码将 Gri
  • 如何将 TestNG 测试输出文件夹配置在 Maven 目标文件夹内?

    Folks 我是 TestNG 的新手 我正在尝试使用 Maven Surefire 插件 和 Eclipse 当我的测试由 Maven 运行时 其报告会按预期放在 target surefire reports 上 但是当由 Eclips
  • 在 Android 上如何从位图获取 RGB 值?

    我想在 Android 上获取位图的 RGB 值 但目前还无法做到这一点 我的目标是获取位图每个像素的 RGB 值 Android 或其他有什么特定的功能吗 我也想知道我需要colorMatrix 功能 这对我的项目非常重要 这可能有点晚了
  • Xamarin.Forms;在屏幕上显示设备上我的 SIM 卡的电话号码

    我想在屏幕上显示我设备的电话号码 在 Xamarin Android 中 代码可以正常工作 但我想使用 Xamarin Forms 中的代码 我已经搜索过 但没有找到任何结果 Android Telephony TelephonyManag
  • 下划线作为 JavaScript 变量?

    In 这个帖子 https stackoverflow com questions 3504499 how to copy table row with clone in jquery and create new unique ids f
  • CMAKE_*_OUTPUT_DIRECTORY 的正确使用

    前言 我是only谈论本地编译 not关于安装项目 这是因为我还没有对适当的研究进行足够的研究install使用 CMake 但如果我的问题直接涉及到 请插话install实践 似乎有可能 TL DR 你在什么场景下not想要将所有正在构建
  • React 中未使用 ES6 fetch 定义 fetch 方法

    我在我的第一个 React js 应用程序中遇到了 fetch 函数的问题 这是我的项目的结构 hello world app components main jsx node modules public build js index h
  • 编写 HTML 电子邮件时的最佳实践和注意事项 [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我开发网站已有十多年了 但很快发现我的许多网络开发习惯在为电子邮件客户端开发时毫无用处 这让我非常沮丧 所以我想问一个问题 对于像我这样不时为
  • React-Leaflet:将地图控制组件放置在地图之外?

    这是我的其他问题的更普遍的版本 从反应传单中的地图中删除缩放控制 https stackoverflow com questions 59432189 remove zoom control from map in react leafle
  • Python 3 和 Tkinter 有缺陷且缓慢

    因此 几个月前 我制作了一个小型 GUI 用于在我正在运行的角色扮演活动中处理 NPC 从那以后我就再也没有接触过 只是现在我需要它 明天 事实上 我有一些奇怪的错误 加载 GUI 似乎工作正常 但是当我开始按下按钮时 麻烦就开始了 起初
  • Git 合并并修复具有两个分支的混合空间和选项卡

    我已经经历了一些类似的 SOQ 但没有找到适合这种情况的适当解决方案 我注意到在许多文件中 用于缩进的制表符和空格混杂在一起 目前我们遵循的编码标准使用 4 个空格作为制表符 虽然这个问题应该在发生时就得到解决 但我现在需要考虑它 并希望修
  • 广播接收器在 Android 6.0 Marshmallow 中不工作

    我刚刚将我的 Nexus 5 更新到 android 6 到目前为止我的应用程序工作正常 但现在广播接收器无法工作 新版本有什么变化吗 这是我尝试过的代码 它适用于以前的版本 但不适用于棉花糖 安卓清单
  • 如何识别模板参数是否为 std::complex?

    如何判断模板参数是否为std complex 我想要一个支持所有数据类型 如 float double int 等 的通用方法 我知道使用 std is same 我可以专门检查给定类型 例如 std complex
  • 使用 System.Text.Json 自定义可选属性的 JSON 序列化器

    我正在尝试实现一个处理两者的 JSON 序列化机制null和缺失的 JSON 值 以便能够在需要时执行部分更新 这样当值缺失时它不会触及数据库中的字段 但当值显式设置为时它会清除它 null 我创建了一个从 Roslyn 复制的自定义结构O