自定义 JSON 协定

System.Text.Json 库为每个 .NET 类型构造一个 JSON 协定,该协定定义应如何序列化和反序列化该类型。 协定派生自类型的形状,其中包括其属性和字段等特征,以及它是否实现 IEnumerableIDictionary 接口。 类型在运行时使用反射或在编译时使用源生成器映射到协定。

从 .NET 7 开始,可以自定义这些 JSON 协定,以便更好地控制如何将类型转换为 JSON,反之亦然。 以下列表仅显示可用于序列化和反序列化的自定义类型的一些示例:

如何选择加入

有两种方法可以插入自定义项。 两者都涉及获取解析程序,其工作是为需要序列化的每种类型提供一个 JsonTypeInfo 实例。

可配置方面

JsonTypeInfo.Kind 属性指示转换器如何序列化给定类型(例如,作为对象或数组),以及其属性是否序列化。 可以查询此属性以确定可以配置类型的 JSON 协定的哪些方面。 有四种不同的类型:

JsonTypeInfo.Kind 说明
JsonTypeInfoKind.Object 转换器将类型序列化为 JSON 对象,并使用其属性。 这种类型用于大多数类和结构类型,并允许最大的灵活性。
JsonTypeInfoKind.Enumerable 转换器将类型序列化为 JSON 数组。 这种类型用于 List<T> 和数组等类型。
JsonTypeInfoKind.Dictionary 转换器将类型序列化为 JSON 对象。 这种类型用于 Dictionary<K, V> 之类的类型。
JsonTypeInfoKind.None 转换器未指定它将如何序列化类型或它将使用哪些 JsonTypeInfo 属性。 这种类型用于 System.Objectintstring 等类型,以及所有使用自定义转换器的类型。

修改键

修饰符是一个 Action<JsonTypeInfo> 或带有 JsonTypeInfo 参数的方法,它获取协定的当前状态作为参数并对协定进行修改。 例如,可以循环访问指定 JsonTypeInfo 上的预填充属性以找到你感兴趣的属性,然后修改其 JsonPropertyInfo.Get 属性(用于序列化)或 JsonPropertyInfo.Set 属性(用于反序列化)。 或者,可以使用 JsonTypeInfo.CreateJsonPropertyInfo(Type, String) 构造一个新属性并将其添加到 JsonTypeInfo.Properties 集合中。

下表显示了可以进行的修改以及如何实现这些修改。

修改 适用 JsonTypeInfo.Kind 如何实现 示例
自定义属性值 JsonTypeInfoKind.Object 修改属性的 JsonPropertyInfo.Get 委托(用于序列化)或 JsonPropertyInfo.Set 委托(用于反序列化)。 属性值
添加或删除属性 JsonTypeInfoKind.Object JsonTypeInfo.Properties 列表中添加或删除项。 序列化专用字段
有条件地序列化属性 JsonTypeInfoKind.Object 修改属性的 JsonPropertyInfo.ShouldSerialize 谓词。 忽略具有特定类型的属性
自定义特定类型的数字处理 JsonTypeInfoKind.None 修改类型的 JsonTypeInfo.NumberHandling 值。 允许 int 值为字符串

示例:递增属性值

请考虑以下示例,其中修饰符在反序列化时通过修改其 JsonPropertyInfo.Set 委托来递增某个属性的值。 除了定义修饰符之外,该示例还引入了一个新属性,用于定位其值应递增的属性。 这是自定义属性的示例。

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    // Custom attribute to annotate the property
    // we want to be incremented.
    [AttributeUsage(AttributeTargets.Property)]
    class SerializationCountAttribute : Attribute
    {
    }

    // Example type to serialize and deserialize.
    class Product
    {
        public string Name { get; set; } = "";
        [SerializationCount]
        public int RoundTrips { get; set; }
    }

    public class SerializationCountExample
    {
        // Custom modifier that increments the value
        // of a specific property on deserialization.
        static void IncrementCounterModifier(JsonTypeInfo typeInfo)
        {
            foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
            {
                if (propertyInfo.PropertyType != typeof(int))
                    continue;

                object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty<object>();
                SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null;

                if (attribute != null)
                {
                    Action<object, object?>? setProperty = propertyInfo.Set;
                    if (setProperty is not null)
                    {
                        propertyInfo.Set = (obj, value) =>
                        {
                            if (value != null)
                            {
                                // Increment the value by 1.
                                value = (int)value + 1;
                            }

                            setProperty (obj, value);
                        };
                    }
                }
            }
        }

        public static void RunIt()
        {
            var product = new Product
            {
                Name = "Aquafresh"
            };

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { IncrementCounterModifier }
                }
            };

            // First serialization and deserialization.
            string serialized = JsonSerializer.Serialize(product, options);
            Console.WriteLine(serialized);
            // {"Name":"Aquafresh","RoundTrips":0}

            Product deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 1

            // Second serialization and deserialization.
            serialized = JsonSerializer.Serialize(deserialized, options);
            Console.WriteLine(serialized);
            // { "Name":"Aquafresh","RoundTrips":1}

            deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
            Console.WriteLine($"{deserialized.RoundTrips}");
            // 2
        }
    }
}

请注意,在输出中,每次反序列化 Product 实例时,RoundTrips 的值都会递增。

示例:序列化专用字段

默认情况下,System.Text.Json 忽略专用字段和属性。 此示例添加了一个新的类范围属性 JsonIncludePrivateFieldsAttribute,以更改该默认值。 如果修饰符找到类型上的属性,它会将类型上的所有专用字段作为新属性添加到 JsonTypeInfo

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class JsonIncludePrivateFieldsAttribute : Attribute { }

    [JsonIncludePrivateFields]
    public class Human
    {
        private string _name;
        private int _age;

        public Human()
        {
            // This constructor should be used only by deserializers.
            _name = null!;
            _age = 0;
        }

        public static Human Create(string name, int age)
        {
            Human h = new()
            {
                _name = name,
                _age = age
            };

            return h;
        }

        [JsonIgnore]
        public string Name
        {
            get => _name;
            set => throw new NotSupportedException();
        }

        [JsonIgnore]
        public int Age
        {
            get => _age;
            set => throw new NotSupportedException();
        }
    }

    public class PrivateFieldsExample
    {
        static void AddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
                return;

            if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false))
                return;

            foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
            {
                JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
                jsonPropertyInfo.Get = field.GetValue;
                jsonPropertyInfo.Set = field.SetValue;

                jsonTypeInfo.Properties.Add(jsonPropertyInfo);
            }
        }

        public static void RunIt()
        {
            var options = new JsonSerializerOptions
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { AddPrivateFieldsModifier }
                }
            };

            var human = Human.Create("Julius", 37);
            string json = JsonSerializer.Serialize(human, options);
            Console.WriteLine(json);
            // {"_name":"Julius","_age":37}

            Human deserializedHuman = JsonSerializer.Deserialize<Human>(json, options)!;
            Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]");
            // [Name=Julius; Age=37]
        }
    }
}

提示

如果专用字段名称以下划线开头,请考虑在将字段添加为新的 JSON 属性时从名称中删除下划线。

示例:忽略具有特定类型的属性

也许你的模型有着具有你不想向用户公开的特定名称或类型的属性。 例如,你可能有一个属性,用于存储凭据或一些在有效负载中无用的信息。

以下示例显示了如何筛选出具有特定类型 SecretHolder 的属性。 它通过使用 IList<T> 扩展方法从 JsonTypeInfo.Properties 列表中删除任何具有指定类型的属性来实现此目的。 筛选出的属性会完全从协定中消失,这意味着 System.Text.Json 在序列化或反序列化期间都不会查看它们。

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    class ExampleClass
    {
        public string Name { get; set; } = "";
        public SecretHolder? Secret { get; set; }
    }

    class SecretHolder
    {
        public string Value { get; set; } = "";
    }

    class IgnorePropertiesWithType
    {
        private readonly Type[] _ignoredTypes;

        public IgnorePropertiesWithType(params Type[] ignoredTypes)
            => _ignoredTypes = ignoredTypes;

        public void ModifyTypeInfo(JsonTypeInfo ti)
        {
            if (ti.Kind != JsonTypeInfoKind.Object)
                return;

            ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType));
        }
    }

    public class IgnoreTypeExample
    {
        public static void RunIt()
        {
            var modifier = new IgnorePropertiesWithType(typeof(SecretHolder));

            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { modifier.ModifyTypeInfo }
                }
            };

            ExampleClass obj = new()
            {
                Name = "Password",
                Secret = new SecretHolder { Value = "MySecret" }
            };

            string output = JsonSerializer.Serialize(obj, options);
            Console.WriteLine(output);
            // {"Name":"Password"}
        }
    }

    public static class ListHelpers
    {
        // IList<T> implementation of List<T>.RemoveAll method.
        public static void RemoveAll<T>(this IList<T> list, Predicate<T> predicate)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (predicate(list[i]))
                {
                    list.RemoveAt(i--);
                }
            }
        }
    }
}

示例:允许 int 值为字符串

也许输入 JSON 可以用引号括起一种数字类型,但对其他类型不使用引号。 如果你可以控制该类,则可以在类型上放置 JsonNumberHandlingAttribute 来解决此问题,但你并不能。 在 .NET 7 之前,你需要编写一个自定义转换器来修复此行为,这需要编写相当多的代码。 使用协定自定义,可以自定义任何类型的数字处理行为。

以下示例更改所有 int 值的行为。 可以轻松调整该示例以应用于任何类型或任何类型的特定属性。

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Serialization
{
    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    public class AllowIntsAsStringsExample
    {
        static void SetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo)
        {
            if (jsonTypeInfo.Type == typeof(int))
            {
                jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
            }
        }

        public static void RunIt()
        {
            JsonSerializerOptions options = new()
            {
                TypeInfoResolver = new DefaultJsonTypeInfoResolver
                {
                    Modifiers = { SetNumberHandlingModifier }
                }
            };

            // Triple-quote syntax is a C# 11 feature.
            Point point = JsonSerializer.Deserialize<Point>("""{"X":"12","Y":"3"}""", options)!;
            Console.WriteLine($"({point.X},{point.Y})");
            // (12,3)
        }
    }
}

如果没有允许从字符串中读取 int 值的修饰符,程序结束时会出现异常:

未经处理的异常。 System.Text.Json.JsonException:无法将 JSON 值转换为 System.Int32。 路径:$.X | LineNumber:0 | BytePositionInLine:9。

自定义序列化的其他方法

除了自定义协定外,还有其他方法来影响序列化和反序列化行为,包括:

协定自定义是对这些预先存在的自定义的改进,因为你可能无法访问类型来添加属性,而编写自定义转换器很复杂并且会损害性能。

请参阅