自訂 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 等類型,以及所有使用自訂轉換器的類型。

修飾詞

修飾元是具有 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。

自訂序列化的其他方式

除了自訂合約之外,還有其他方式會影響序列化和還原序列化行為,包括下列各項:

合約自訂是這些既有自訂專案的改善,因為您可能無法存取類型來新增屬性,而撰寫自訂轉換程式很複雜,且會減損效能。

另請參閱