自定义 JSON 协定
System.Text.Json 库为每个 .NET 类型构造一个 JSON 协定,该协定定义应如何序列化和反序列化该类型。 协定派生自类型的形状,其中包括其属性和字段等特征,以及它是否实现 IEnumerable 或 IDictionary 接口。 类型在运行时使用反射或在编译时使用源生成器映射到协定。
从 .NET 7 开始,可以自定义这些 JSON 协定,以便更好地控制如何将类型转换为 JSON,反之亦然。 以下列表仅显示可用于序列化和反序列化的自定义类型的一些示例:
- 序列化专用字段和属性。
- 支持单个属性的多个名称(例如,如果以前的库版本使用不同的名称)。
- 忽略具有特定名称、类型或值的属性。
- 区分显式
null
值和 JSON 有效负载中缺少的值。 - 支持 System.Runtime.Serialization 属性,例如 DataContractAttribute。 有关详细信息,请参阅 System.Runtime.Serialization 属性。
- 如果 JSON 包含不属于目标类型的属性,则引发异常。 有关详细信息,请参阅处理缺少的成员。
如何选择加入
有两种方法可以插入自定义项。 两者都涉及获取解析程序,其工作是为需要序列化的每种类型提供一个 JsonTypeInfo 实例。
通过调用 DefaultJsonTypeInfoResolver() 构造函数来获取 JsonSerializerOptions.TypeInfoResolver 并将自定义操作 添加到其 Modifiers 属性。
例如:
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { MyCustomModifier1, MyCustomModifier2 } } };
如果添加多个修饰符,将按顺序调用它们。
通过编写实现 IJsonTypeInfoResolver 的自定义解析程序。
- 如果未处理某个类型,则 IJsonTypeInfoResolver.GetTypeInfo 应为该类型返回
null
。 - 还可以将自定义解析程序与其他解析程序组合在一起,例如默认解析程序。 解析程序将按顺序查询,直到为该类型返回非空 JsonTypeInfo 值。
- 如果未处理某个类型,则 IJsonTypeInfoResolver.GetTypeInfo 应为该类型返回
可配置方面
该 JsonTypeInfo.Kind 属性指示转换器如何序列化给定类型(例如,作为对象或数组),以及其属性是否序列化。 可以查询此属性以确定可以配置类型的 JSON 协定的哪些方面。 有四种不同的类型:
JsonTypeInfo.Kind |
说明 |
---|---|
JsonTypeInfoKind.Object | 转换器将类型序列化为 JSON 对象,并使用其属性。 这种类型用于大多数类和结构类型,并允许最大的灵活性。 |
JsonTypeInfoKind.Enumerable | 转换器将类型序列化为 JSON 数组。 这种类型用于 List<T> 和数组等类型。 |
JsonTypeInfoKind.Dictionary | 转换器将类型序列化为 JSON 对象。 这种类型用于 Dictionary<K, V> 之类的类型。 |
JsonTypeInfoKind.None | 转换器未指定它将如何序列化类型或它将使用哪些 JsonTypeInfo 属性。 这种类型用于 System.Object、int 和 string 等类型,以及所有使用自定义转换器的类型。 |
修改键
修饰符是一个 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。
自定义序列化的其他方法
除了自定义协定外,还有其他方法来影响序列化和反序列化行为,包括:
- 通过使用从 JsonAttribute 派生的属性,例如 JsonIgnoreAttribute 和 JsonPropertyOrderAttribute。
- 例如,通过修改 JsonSerializerOptions 来设置命名策略或将枚举值序列化为字符串而不是数字。
- 通过编写一个自定义转换器来完成编写 JSON 的实际工作,并在反序列化期间构造一个对象。
协定自定义是对这些预先存在的自定义的改进,因为你可能无法访问类型来添加属性。 此外,编写自定义转换器既复杂又影响性能。