自訂 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
。 - 您也可以將自訂解析程式與其他解析程式結合,例如預設解析程式。 解析程式會依序查詢,直到針對類型傳回非 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 等類型,以及所有使用自訂轉換器的類型。 |
修飾詞
修飾元是具有 JsonTypeInfo 參數的 Action<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 的實際工作,並在還原序列化期間建構物件。
合約自定義是這些預先存在的自定義專案的改善,因為您可能沒有新增屬性的型別存取權。 此外,撰寫自定義轉換器很複雜,而且會損害效能。