如何在 .NET 中撰寫 JSON 序列化 (封送處理) 的自訂轉換器

本文說明如何為 System.Text.Json 命名空間中提供的 JSON 序列化類別,建立自訂轉換器。 如需 System.Text.Json 簡介,請參閱如何在 .NET 中序列化和還原序列化 JSON

「轉換器」是一種類別,可將物件或值轉換成 JSON,以及從 JSON 轉換成物件或值。 System.Text.Json 命名空間具有內建轉換器,適用於大部分對應至 JavaScript 基元的基本類型。 您可以撰寫自定義轉換器來覆寫內建轉換器的預設行為。 例如:

  • 您可能想要 DateTime 以 mm/dd/yyyy 格式來表示值。 預設支援 ISO 8601-1:2019,包括 RFC 3339 設定檔。 如需詳細資訊,請參閱 System.Text.Json 中的 DateTime 與 DateTimeOffset 支援
  • 您可能想要將POCO串行化為 JSON 字串,例如,具有型別 PhoneNumber

您也可以撰寫自訂轉換器,透過新功能來自訂或擴充 System.Text.Json。 本文稍後將說明下列情節:

Visual Basic 無法用於撰寫自訂轉換器,但可以呼叫 C# 程式庫中實作的轉換器。 如需詳細資訊,請參閱 Visual Basic 支援

自訂轉換器模式

建立自訂轉換器有兩種模式:基本模式和中心模式。 中心模式適用於處理 Enum 型別或開放式泛型的轉換器。 基本模式適用於非泛型和封閉式泛型型別。 例如,下列型別的轉換器需要中心模式:

基本模式可以處理的一些型別範例包括:

  • Dictionary<int, string>
  • WeekdaysEnum
  • List<DateTimeOffset>
  • DateTime
  • Int32

基本模式會建立可以處理一種型別的類別。 中心模式會建立類別,以判斷在執行階段需要的特定型別,並動態建立適當的轉換器。

範例基本轉換器

下列範例是會覆寫現有資料類型之預設序列化的轉換器。 轉換器會針對 DateTimeOffset 屬性使用 mm/dd/yyyy 格式。

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                DateTimeOffset.ParseExact(reader.GetString()!,
                    "MM/dd/yyyy", CultureInfo.InvariantCulture);

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(dateTimeValue.ToString(
                    "MM/dd/yyyy", CultureInfo.InvariantCulture));
    }
}

範例中心模式轉換器

下列程式碼顯示可搭配 Dictionary<Enum,TValue> 使用的自訂轉換器。 程式碼會遵循中心模式,因為第一個泛型型別參數是 Enum,而第二個是開放式。 CanConvert 方法只會針對具有兩個泛型參數的 Dictionary 傳回 true,而第一個泛型參數是 Enum 型別。 內部轉換器會取得現有的轉換器,以處理執行階段中為 TValue 提供的型別。

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

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type[] typeArguments = type.GetGenericArguments();
            Type keyType = typeArguments[0];
            Type valueType = typeArguments[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    [keyType, valueType]),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: [options],
                culture: null)!;

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string? propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    reader.Read();
                    TValue value = _valueConverter.Read(ref reader, _valueType, options)!;

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    string propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    _valueConverter.Write(writer, value, options);
                }

                writer.WriteEndObject();
            }
        }
    }
}

遵循基本模式的步驟

下列步驟說明如何遵循基本模式來建立轉換器:

  • 建立衍生自 JsonConverter<T> 的類別,其中 T 是要序列化和還原序列化的型別。
  • 覆寫 Read 方法以還原序列化傳入 JSON,並將其轉換成類型 T。 使用傳遞至方法的 Utf8JsonReader 來讀取 JSON。 您不需要擔心處理部分資料,因為序列化程式會傳遞目前 JSON 範圍的所有資料。 因此,不需要呼叫 SkipTrySkip,或是驗證 Read 傳回 true
  • 覆寫 Write 方法,將 T 型別的傳入物件序列化。 使用傳遞至方法的 Utf8JsonWriter 來寫入 JSON。
  • 只有在需要時才覆寫 CanConvert 方法。 當要轉換的型別為 T 時,預設實作會傳回 true。 因此,僅支援 T 型別的轉換器不需要覆寫此方法。 如需需要覆寫此方法的轉換器範例,請參閱本文稍後的多形還原序列化一節。

您可以將內建轉換器原始程式碼當作撰寫自訂轉換器的參考實作。

遵循中心模式的步驟

下列步驟說明如何遵循中心模式來建立轉換器:

  • 建立衍生自 JsonConverterFactory 的類別。
  • 覆寫 CanConvert 方法,以在所要轉換型別是轉換器可以處理的型別時傳回 true。 例如,若是針對 List<T> 的轉換器,其可能只處理 List<int>List<string>List<DateTime>
  • 覆寫 CreateConverter 方法,以傳回轉換器類別的執行個體,其會處理執行階段所提供要轉換的型別。
  • 建立 CreateConverter 方法具現化的轉換器類別。

開放式泛型需要中心模式,因為將物件轉換成字串及將字串轉換成物件的程式碼在所有型別中都不盡相同。 開放式泛型型別的轉換器 (例如 List<T>) 必須在幕後為封閉式泛型型別 (例如 List<DateTime>) 建立轉換器。 必須撰寫程式碼,以處理轉換器可以處理的每個封閉式泛型型別。

Enum 型別類似於開放式泛型型別:Enum 的轉換器必須在幕後針對特定 Enum (例如 WeekdaysEnum) 建立轉換器。

Read 方法中使用 Utf8JsonReader

若您的轉換器正在轉換 JSON 物件,則當 Read 方法開始時,會將 Utf8JsonReader 放在開始物件權杖上。 然後,您必須讀取該物件中的所有權杖,並使用位於對應結束物件權杖上的讀取器結束方法。 若您讀取超過物件的結尾,或若您在到達對應的結束權杖之前停止,則會收到 JsonException 例外狀況,指出:

轉換器 'ConverterName' 讀取太多或不足。

如需範例,請參閱上述中心模式範例轉換器。 Read 方法會先確認讀取器位於起始物件權杖上。 在發現位於下一個結束物件權杖之前,讀取器會持續讀取。 讀取器會在下一個結束物件權杖上停止,因為沒有任何介入的起始物件權杖會指出物件中的物件。 若您要轉換陣列,則會套用與開始權杖及結束權杖相同的規則。 如需範例,請參閱本文中稍後的 Stack<T> 範例程式碼。

錯誤處理

序列化程式會提供例外狀況型別 JsonExceptionNotSupportedException 的特殊處理。

JsonException

若您在沒有訊息的的情況下擲回 JsonException,則序列化程式會建立包含導致錯誤的 JSON 部分路徑訊息。 例如,陳述式 throw new JsonException() 會產生類似下列範例的錯誤訊息:

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

若您提供訊息 (例如 throw new JsonException("Error occurred")),則序列化程式仍會設定 PathLineNumberBytePositionInLine 屬性。

NotSupportedException

若您擲回 NotSupportedException,則一律會在訊息中取得路徑資訊。 若您提供訊息,則會將路徑資訊附加至該訊息。 例如,陳述式 throw new NotSupportedException("Error occurred.") 會產生類似下列範例的錯誤訊息:

Error occurred. The unsupported member type is located on type
'System.Collections.Generic.Dictionary`2[Samples.SummaryWords,System.Int32]'.
Path: $.TemperatureRanges | LineNumber: 4 | BytePositionInLine: 24

擲回例外狀況型別的時機

當 JSON 承載包含對要還原序列化之型別無效的權杖時,會擲回 JsonException

當您想要不允許特定型別時,請擲回 NotSupportedException。 此例外狀況是序列化程式針對不支援的型別所自動擲回。 例如,基於安全性原因不支援 System.Type,因此嘗試還原序列化會導致 NotSupportedException

您可以視需要擲回其他例外狀況,但這些例外狀況不會自動包含 JSON 路徑資訊。

註冊自訂轉換器

「註冊」自訂轉換器,讓 SerializeDeserialize 方法可使用該自訂轉換器。 選擇下列其中一個方法:

註冊範例 - 轉換器集合

以下範例讓 DateTimeOffsetJsonConverter 成為 DateTimeOffset 型別屬性的預設值:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new DateTimeOffsetJsonConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

假設您將下列型別的執行個體序列化:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

以下範例是顯示已使用自訂轉換器的 JSON 輸出:

{
  "Date": "08/01/2019",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

下列程式碼使用相同的方法,利用自訂 DateTimeOffset 轉換器進行還原序列化:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions)!;

註冊範例 - 屬性上的 [JsonConverter]

下列程式碼會選取 Date 屬性的自訂轉換器:

public class WeatherForecastWithConverterAttribute
{
    [JsonConverter(typeof(DateTimeOffsetJsonConverter))]
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

要將 WeatherForecastWithConverterAttribute 序列化的程式碼不需要使用 JsonSerializeOptions.Converters

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

要還原序列化的程式碼也不需要使用 Converters

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString)!;

註冊範例 - 型別上的 [JsonConverter]

以下程式碼會建立結構,並將 [JsonConverter] 屬性套用至該結構:

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            Degrees = degrees;
            IsCelsius = celsius;
        }

        public int Degrees { get; }
        public bool IsCelsius { get; }
        public bool IsFahrenheit => !IsCelsius;

        public override string ToString() =>
            $"{Degrees}{(IsCelsius ? "C" : "F")}";

        public static Temperature Parse(string input)
        {
            int degrees = int.Parse(input.Substring(0, input.Length - 1));
            bool celsius = input.Substring(input.Length - 1) == "C";

            return new Temperature(degrees, celsius);
        }
    }
}

以下是上述結構的自訂轉換器:

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

namespace SystemTextJsonSamples
{
    public class TemperatureConverter : JsonConverter<Temperature>
    {
        public override Temperature Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                Temperature.Parse(reader.GetString()!);

        public override void Write(
            Utf8JsonWriter writer,
            Temperature temperature,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(temperature.ToString());
    }
}

結構上的 [JsonConverter] 屬性會將自訂轉換器註冊為 Temperature 型別屬性的預設值。 當您將轉換器序列化或還原序列化時,轉換器會自動用於下列型別的 TemperatureCelsius 屬性:

public class WeatherForecastWithTemperatureStruct
{
    public DateTimeOffset Date { get; set; }
    public Temperature TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

轉換器註冊優先順序

在序列化或還原序列化期間,會依下列順序為每個 JSON 元素選擇轉換器,從最高優先順序列到最低優先順序:

  • 已套用至屬性的 [JsonConverter]
  • 已新增至 Converters 集合的轉換器。
  • 已套用至自訂實值型別或 POCO 的 [JsonConverter]

若在 Converters 集合中註冊某個型別的多個自訂轉換器,則會使用針對 CanConvert 傳回 true 的第一個轉換器。

只有在未註冊適用的自訂轉換器時,才會選擇內建轉換器。

常見情節的轉換器範例

下列各節提供轉換器範例,可解決內建功能無法處理的一些常見情節。

如需範例 DataTable 轉換器,請參閱支援的集合類型 (機器翻譯)

將推斷的型別還原序列化為物件屬性

還原序列化為 object 型別的屬性時,會建立 JsonElement 物件。 原因是還原序列化程式不知道要建立的 CLR 型別,也不會嘗試猜測。 例如,若 JSON 屬性有 "true",則還原序列化程式不會推斷值為 Boolean,且若元素有 "01/01/2019",則還原序列化程式不會將其推斷為 DateTime

型別推斷可能不正確。 若還原序列化程式將沒有小數點的 JSON 數字剖析為 long,則若值原本序列化為 ulongBigInteger,可能會導致超出範圍的問題。 若數字原本序列化為 decimal,則將具有小數點的數字剖析為 double,可能會失去精確度。

對於需要型別推斷的情節,下列程式碼會顯示 object 屬性的自訂轉換器。 程式碼會轉換:

  • truefalse,並將其轉換成 Boolean
  • 沒有小數點的數字,並將其轉換成 long
  • 具有小數點的數字,並將其轉換成 double
  • 日期,並將其轉換成 DateTime
  • 字串,並將其轉換成 string
  • 任何其他項目,並將其轉換成 JsonElement
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterInferredTypesToObject
{
    public class ObjectToInferredTypesConverter : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString()!,
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
    }

    public class WeatherForecast
    {
        public object? Date { get; set; }
        public object? TemperatureCelsius { get; set; }
        public object? Summary { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            string jsonString = """
                {
                  "Date": "2019-08-01T00:00:00-07:00",
                  "TemperatureCelsius": 25,
                  "Summary": "Hot"
                }
                """;

            WeatherForecast weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString)!;
            Console.WriteLine($"Type of Date property   no converter = {weatherForecast.Date!.GetType()}");

            var options = new JsonSerializerOptions();
            options.WriteIndented = true;
            options.Converters.Add(new ObjectToInferredTypesConverter());
            weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, options)!;
            Console.WriteLine($"Type of Date property with converter = {weatherForecast.Date!.GetType()}");

            Console.WriteLine(JsonSerializer.Serialize(weatherForecast, options));
        }
    }
}

// Produces output like the following example:
//
//Type of Date property   no converter = System.Text.Json.JsonElement
//Type of Date property with converter = System.DateTime
//{
//  "Date": "2019-08-01T00:00:00-07:00",
//  "TemperatureCelsius": 25,
//  "Summary": "Hot"
//}

此範例顯示轉換器程式碼與具有 object 屬性的 WeatherForecast 類別。 Main 方法會先不使用轉換器,然後再使用轉換器,將 JSON 字串還原序列化為 WeatherForecast 執行個體。 主控台輸出顯示,若沒有轉換器,則 Date 屬性的執行階段型別為 JsonElement; 若有轉換器,則執行階段型別為 DateTime

System.Text.Json.Serialization 命名空間中的單元測試資料夾有更多自訂轉換器範例,可處理還原序列化為 object 屬性。

支援多形還原序列化

.NET 7 同時支援多型序列化與還原序列化。 然而,在舊版 .NET 中,多型序列化支援有限,且不支援還原序列化。 若您使用 .NET 6 或舊版,則還原序列化需要自訂轉換器。

例如,假設您有 Person 抽象基底類別,其中包含 EmployeeCustomer 衍生類別。 多形還原序列化表示您可以在設計階段將 Person 指定為還原序列化目標,且 JSON 中的 CustomerEmployee 物件會在執行階段正確還原序列化。 在還原序列化期間,您必須找出在 JSON 中識別必要型別的線索。 可用的線索種類會隨著每個情節而有所不同。 例如,可能可使用鑑別子屬性,或者您可能必須依賴特定屬性 (property) 是否存在。 目前的 System.Text.Json 版本不提供屬性 (attribute) 來指定如何處理多形還原序列化情節,因此需要自訂轉換器。

下列程式碼顯示一個基底類別、兩個衍生類別,以及一個適用於這些類別的自訂轉換器。 該轉換器會使用鑑別子屬性來執行多形還原序列化。 型別鑑別子不在類別定義中,而是在序列化期間建立,並在還原序列化期間讀取。

重要

範例程式碼需要 JSON 物件名稱/值對保持順序,這不是 JSON 的標準需求。

public class Person
{
    public string? Name { get; set; }
}

public class Customer : Person
{
    public decimal CreditLimit { get; set; }
}

public class Employee : Person
{
    public string? OfficeNumber { get; set; }
}
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {
        enum TypeDiscriminator
        {
            Customer = 1,
            Employee = 2
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(Person).IsAssignableFrom(typeToConvert);

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string? propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string? officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string? name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(
            Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            if (person is Customer customer)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Customer);
                writer.WriteNumber("CreditLimit", customer.CreditLimit);
            }
            else if (person is Employee employee)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Employee);
                writer.WriteString("OfficeNumber", employee.OfficeNumber);
            }

            writer.WriteString("Name", person.Name);

            writer.WriteEndObject();
        }
    }
}

下列程式碼會註冊轉換器:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());

轉換器可以使用相同轉換器來還原序列化所建立的 JSON,例如:

[
  {
    "TypeDiscriminator": 1,
    "CreditLimit": 10000,
    "Name": "John"
  },
  {
    "TypeDiscriminator": 2,
    "OfficeNumber": "555-1234",
    "Name": "Nancy"
  }
]

上述範例中的轉換器程式碼會手動讀取及寫入每個屬性。 替代方式是呼叫 DeserializeSerialize,以執行某些工作。 例如,請參閱此 StackOverflow 貼文 (英文)

執行多形還原序列化的替代方式

您可以在 Read 方法中呼叫 Deserialize

  • 製作 Utf8JsonReader 執行個體的複製品。 由於 Utf8JsonReader 是結構,因此這只需要指派陳述式。
  • 使用複製品來讀取鑑別子權杖。
  • 您知道所需的型別之後,請使用原始 Reader 執行個體呼叫 Deserialize。 您可以呼叫 Deserialize,因為原始 Reader 執行個體仍然位於讀取開始物件權杖的位置。

此方法的缺點是您無法傳入向 Deserialize 註冊轉換器的原始選項執行個體。 這麼做會導致堆疊溢位,如必要屬性中所述。 下列範例顯示使用此替代方式的 Read 方法:

public override Person Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    Utf8JsonReader readerClone = reader;

    if (readerClone.TokenType != JsonTokenType.StartObject)
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.PropertyName)
    {
        throw new JsonException();
    }

    string? propertyName = readerClone.GetString();
    if (propertyName != "TypeDiscriminator")
    {
        throw new JsonException();
    }

    readerClone.Read();
    if (readerClone.TokenType != JsonTokenType.Number)
    {
        throw new JsonException();
    }

    TypeDiscriminator typeDiscriminator = (TypeDiscriminator)readerClone.GetInt32();
    Person person = typeDiscriminator switch
    {
        TypeDiscriminator.Customer => JsonSerializer.Deserialize<Customer>(ref reader)!,
        TypeDiscriminator.Employee => JsonSerializer.Deserialize<Employee>(ref reader)!,
        _ => throw new JsonException()
    };
    return person;
}

支援 Stack 類型的來回行程

若您將 JSON 字串還原序列化為 Stack 物件,然後將該物件序列化,則堆疊的內容會以反向順序排列。 此行為適用於下列類型與介面,以及衍生自這些的使用者定義型別:

若要支援在堆疊中保留原始順序的序列化與還原序列化,則需要自訂轉換器。

下列程式碼顯示自訂轉換程式,可往返 Stack<T> 物件:

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

namespace SystemTextJsonSamples
{
    public class JsonConverterFactoryForStackOfT : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsGenericType
            && typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>);

        public override JsonConverter CreateConverter(
            Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(typeToConvert.IsGenericType &&
                typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>));

            Type elementType = typeToConvert.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(JsonConverterForStackOfT<>)
                    .MakeGenericType(new Type[] { elementType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null)!;

            return converter;
        }
    }

    public class JsonConverterForStackOfT<T> : JsonConverter<Stack<T>>
    {
        public override Stack<T> Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
            reader.Read();

            var elements = new Stack<T>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                elements.Push(JsonSerializer.Deserialize<T>(ref reader, options)!);

                reader.Read();
            }

            return elements;
        }

        public override void Write(
            Utf8JsonWriter writer, Stack<T> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            var reversed = new Stack<T>(value);

            foreach (T item in reversed)
            {
                JsonSerializer.Serialize(writer, item, options);
            }

            writer.WriteEndArray();
        }
    }
}

下列程式碼會註冊轉換器:

var options = new JsonSerializerOptions
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

列舉字串還原序列化的命名原則

內建 JsonStringEnumConverter 預設可以將列舉的字串值序列化及還原序列化。 其可在沒有指定的命名原則或具有 CamelCase 命名原則的情況下運作。 其不支援其他命名原則,例如蛇形命名法。 如需在使用蛇形命名法原則時,可支援往返列舉字串值的自訂轉換程式碼資訊,請參閱 GitHub 問題 dotnet/runtime #31619 (英文)。 或者,升級至 .NET 7 或更新版本,以提供在往返列舉字串值時套用命名原則的內建支援。

使用預設系統轉換器

在某些情況下,建議您在自訂轉換器中使用預設系統轉換器。 若要這樣做,請從 JsonSerializerOptions.Default 屬性取得系統轉換器,如下列範例所示:

public class MyCustomConverter : JsonConverter<int>
{
    private readonly static JsonConverter<int> s_defaultConverter = 
        (JsonConverter<int>)JsonSerializerOptions.Default.GetConverter(typeof(int));

    // Custom serialization logic
    public override void Write(
        Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }

    // Fall back to default deserialization logic
    public override int Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return s_defaultConverter.Read(ref reader, typeToConvert, options);
    }
}

處理 Null 值

序列化程式預設會處理 Null 值,如下所示:

  • 如需參考型別和 Nullable<T> 型別:

    • 其不會在序列化時,將 null 傳遞至自訂轉換器。
    • 其不會在還原序列化時,將 JsonTokenType.Null 傳遞至自訂轉換器。
    • 其會在還原序列化時傳回 null 執行個體。
    • 其會在序列化時直接使用寫入器寫入 null
  • 針對不可為 Null 的實值型別:

    • 其會在還原序列化時,將 JsonTokenType.Null 傳遞至自訂轉換器。 (若沒有自訂轉換器可用,則內部轉換器會為該型別擲回 JsonException 例外狀況。)

此 Null 處理行為主要是藉由略過對轉換器的額外呼叫,將效能最佳化。 此外,其可避免強制針對可為 Null 型別的轉換器,在每次 ReadWrite 方法覆寫開始時檢查 null

若要讓自訂轉換器處理 null 參考或實值型別,請覆寫 JsonConverter<T>.HandleNull 以傳回 true,如下列範例所示:

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

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

        [JsonConverter(typeof(DescriptionConverter))]
        public string? Description { get; set; }
    }

    public class DescriptionConverter : JsonConverter<string>
    {
        public override bool HandleNull => true;

        public override string Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
            reader.GetString() ?? "No description provided.";

        public override void Write(
            Utf8JsonWriter writer,
            string value,
            JsonSerializerOptions options) =>
            writer.WriteStringValue(value);
    }

    public class Program
    {
        public static void Main()
        {
            string json = @"{""x"":1,""y"":2,""Description"":null}";

            Point point = JsonSerializer.Deserialize<Point>(json)!;
            Console.WriteLine($"Description: {point.Description}");
        }
    }
}

// Produces output like the following example:
//
//Description: No description provided.

保留參考

每次呼叫 SerializeDeserialize 時,預設只會快取參考資料。 若要將一個 Serialize/Deserialize 呼叫的參考保存至另一個呼叫,請將 ReferenceResolver 執行個體的根目錄設為 Serialize/Deserialize 的呼叫位置。 下列程式碼顯示此情節的範例:

  • 您撰寫 Company 型別的自訂轉換器。
  • 您不想手動序列化 Supervisor 屬性,其為 Employee。 您想將其委派給序列化程式,也想保留您已儲存的參考。

以下是 EmployeeCompany 類別:

public class Employee
{
    public string? Name { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee>? DirectReports { get; set; }
    public Company? Company { get; set; }
}

public class Company
{
    public string? Name { get; set; }
    public Employee? Supervisor { get; set; }
}

轉換器如下所示:

class CompanyConverter : JsonConverter<Company>
{
    public override Company Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Company value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        writer.WriteString("Name", value.Name);

        writer.WritePropertyName("Supervisor");
        JsonSerializer.Serialize(writer, value.Supervisor, options);

        writer.WriteEndObject();
    }
}

衍生自 ReferenceResolver 的類別會將參考儲存於字典中:

class MyReferenceResolver : ReferenceResolver
{
    private uint _referenceCount;
    private readonly Dictionary<string, object> _referenceIdToObjectMap = new ();
    private readonly Dictionary<object, string> _objectToReferenceIdMap = new (ReferenceEqualityComparer.Instance);

    public override void AddReference(string referenceId, object value)
    {
        if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
        {
            throw new JsonException();
        }
    }

    public override string GetReference(object value, out bool alreadyExists)
    {
        if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
        {
            alreadyExists = true;
        }
        else
        {
            _referenceCount++;
            referenceId = _referenceCount.ToString();
            _objectToReferenceIdMap.Add(value, referenceId);
            alreadyExists = false;
        }

        return referenceId;
    }

    public override object ResolveReference(string referenceId)
    {
        if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
        {
            throw new JsonException();
        }

        return value;
    }
}

衍生自 ReferenceHandler 的類別會保留 MyReferenceResolver 的執行個體,且只有在需要時才會建立新的執行個體 (在此範例中採用名為 Reset 的方法):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();

    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();
}

當範例程式碼呼叫序列化程式時,該序列化程式會使用 JsonSerializerOptions 執行個體,其中 ReferenceHandler 屬性已設定為 MyReferenceHandler 的執行個體。 當您遵循此模式時,請務必在完成序列化時重設 ReferenceResolver 字典,以防止其無限成長。

var options = new JsonSerializerOptions();

options.Converters.Add(new CompanyConverter());
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.WriteIndented = true;

string str = JsonSerializer.Serialize(tyler, options);

// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();

上述範例只會進行序列化,但可以採用類似的方法來還原序列化。

其他自訂轉換器範例

從 Newtonsoft.Json 移轉至 System.Text.Json 一文包含其他自訂轉換器範例。

System.Text.Json.Serialization 原始程式碼中的單元測試資料夾包含其他自訂轉換器範例,例如:

若您需要建立可修改現有內建轉換器行為的轉換器,可以取得現有轉換器的原始程式碼,作為自訂的起點。

其他資源