.NET 中的 System.Text.Json 自定义转换器

.NET 中的 System.Text.Json 自定义转换器

本文介绍如何为 System.Text.Json 命名空间中提供的 JSON 序列化类创建自定义转换器。 有关 System.Text.Json 简介,请参阅如何在 .NET 中对 JSON 数据进行序列化和反序列化。

转换器是一种将对象或值与 JSON 相互转换的类。
System.Text.Json 命名空间为映射到 JavaScript 基元的大多数基元类型提供内置转换器。 可以编写自定义转换器来替代内置转换器的默认行为。 例如:

  • 你可能希望通过 MM/dd/yyyy 格式来表示 DateTime 值。 默认情况下,支持 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

基本模式创建的类可以处理一种类型。 工厂模式创建一个类,该类确定在运行时需要特定类型,并动态创建相应的转换器。

从 .NET 11 开始,你也可以直接将 [JsonConverter] 用于泛型类型上的开放泛型转换器,而无需使用工厂模式。 有关更多信息,请参阅将开放泛型转换器与 [JsonConverter] 一起使用。

示例基本转换器

下面的示例是一个转换器,可重写现有数据类型的默认序列化。 该转换器将 MM/dd/yyyy 格式用于 DateTimeOffset 属性。

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> 的自定义转换器。它采用工厂模式:第一个泛型参数是某种枚举类型,第二个参数保持开放。CanConvert 只对具有两个泛型参数、且第一个参数为枚举类型的 Dictionary 返回 true。内部转换器复用已有转换器,处理运行时实际提供的 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 范围的所有数据。 因此,不需要调用 Skip 或 TrySkip,也不需要验证 Read 是否返回 true。
  • 重写 Write 方法以序列化 T 类型的传入对象。 使用传递给方法的 Utf8JsonWriter 来写入 JSON。
  • 仅在需要时重写 CanConvert。当目标类型为 T 时,默认实现返回 true,因此只支持 T 的转换器不必重写它。确实需要重写的例子见后面的多态反序列化部分。

可以参阅内置转换器源代码作为用于编写自定义转换器的参考实现。

遵循工厂模式的步骤

以下步骤说明如何遵循工厂模式来创建转换器:

  • 创建一个从 JsonConverterFactory 派生的类。
  • 重写 CanConvert 方法,以在要转换的类型是转换器可处理的类型时返回 true。 例如,如果转换器适用于 List<T>,则它可能仅处理 List<int>、List<string> 和 List<DateTime>。
  • 重写 CreateConverter 方法,以返回将处理在运行时提供的要转换的类型的转换器类实例。
  • 创建 CreateConverter 方法实例化的转换器类。

.NET 10 及更早版本中开放泛型需要工厂模式,因为用于将对象转换为字符串和从字符串转换的代码对于所有类型都不相同。 适用于开放式泛型类型(例如 List<T>)的转换器必须在幕后为封闭式泛型类型(例如 List<DateTime>)创建转换器。 必须编写代码来处理转换器可处理的每种封闭式泛型类型。 从 .NET 11 开始,对于较简单的情况,可以直接配合 [JsonConverter] 使用开放泛型转换器。 有关更多信息,请参阅将开放泛型转换器与 [JsonConverter] 一起使用。

Enum 类型类似于开放式泛型类型:适用于 Enum 的转换器必须在幕后为特定 Enum(例如WeekdaysEnum)创建转换器。

将开放泛型转换器与 [JsonConverter] 一起使用

从 .NET 11 开始,只要泛型参数总数匹配,JsonConverterAttribute 就支持在泛型类型上使用开放泛型转换器,例如 [JsonConverter(typeof(OptionConverter<>))],无需实现 JsonConverterFactory。序列化程序会自动构造封闭泛型转换器。

与上述基于反射的工厂示例不同,源生成器在编译时解析具体的封闭转换器类型。 如果转换器本身使用与 AOT 兼容的 API,并且生成的上下文为其处理的类型提供元数据,则可以将此模式与源代码生成和 Native AOT 配合使用。 该 OptionConverter<T> 示例使用 options.GetTypeInfo<T>() 获取其内部值的元数据。

定义泛型类型

使用 [JsonConverter] 标注你的泛型类型,并指定开放泛型转换器类型。 转换器和目标类型的总泛型参数个数必须匹配:

[JsonConverter(typeof(OptionConverter<>))]
public readonly struct Option<T>
{
    public bool HasValue { get; }
    public T Value { get; }

    public Option(T value)
    {
        HasValue = true;
        Value = value;
    }
}

实现转换器

使用与目标类型相同的泛型类型参数从 JsonConverter<T> 派生转换器:

public sealed class OptionConverter<T> : JsonConverter<Option<T>>
{
    public override bool HandleNull => true;

    public override Option<T> Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
        {
            return default;
        }

        T value = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<T>())!;
        return new Option<T>(value);
    }

    public override void Write(
        Utf8JsonWriter writer, Option<T> value, JsonSerializerOptions options)
    {
        if (!value.HasValue)
        {
            writer.WriteNullValue();
            return;
        }

        JsonSerializer.Serialize(writer, value.Value, options.GetTypeInfo<T>());
    }
}

序列化和反序列化

直接使用 Option<T> 值 – 无需其他配置:

string json = JsonSerializer.Serialize(new Option<int>(42));
Console.WriteLine(json);
// Output: 42

Option<int> option = JsonSerializer.Deserialize<Option<int>>(json);
Console.WriteLine(option.Value);
// Output: 42

转换器还适用于使用 Option<T> 的类型的属性:

public class UserProfile
{
    public string Name { get; set; } = "";
    public Option<string> Nickname { get; set; }
    public Option<int> Age { get; set; }
}

工作原理

当序列化程序遇到用开放泛型转换器(例如 [JsonConverter(typeof(OptionConverter<>))])进行批注的类型时,它会:

  1. 检测转换器类型是否为开放泛型类型。
  2. 验证转换器上的泛型类型参数总数是否与目标类型匹配。
  3. 使用目标类型的类型参数构造封闭泛型转换器(例如 OptionConverter<int>)。

这种自动构造也适用于:

  • 多个类型参数:例如,[JsonConverter(typeof(ResultConverter<,>))] 位于 Result<T, TError> 上。
  • 嵌套泛型转换器:在其他泛型类型内嵌套的转换器类型。
  • 属性级属性: [JsonConverter(typeof(OptionConverter<>))] 应用于单个属性。

何时使用工厂模式与开放泛型转换器

在以下情况下配合 [JsonConverter] 使用开放泛型转换器:

  • 转换器具有与转换的类型相同的类型参数数。
  • 你希望注册方式尽可能简单,无需任何工厂样板代码。

在以下情况下继续使用 JsonConverterFactory :

  • 转换器需要处理各种泛型 Arity 的类型(例如,单个工厂可同时为 Result<T> 和 Result<T, TError> 创建转换器)。
  • 你需要在 CanConvert 中编写自定义逻辑,以确定转换器支持哪些类型。
  • 通过 JsonSerializerOptions.Converters 注册转换器,而不是通过 [JsonConverter] 属性。

注释

在运行时,对非泛型类型使用开放泛型转换器,或者泛型参数总数不匹配时,将引发 InvalidOperationException。 消息标识转换器和目标类型。

在 Read 方法中使用 Utf8JsonReader

转换 JSON 对象时,Read 方法开始执行时,Utf8JsonReader 位于对象起始标记。你必须读取该对象的全部内容,并在对应的对象结束标记处退出。读取过多或过少都会导致 JsonException:

转换器“ConverterName”读取过多或过少。

如需示例,请参阅前面的工厂模式示例转换器。
Read 方法首先会验证读取器是否定位到起始对象标记上。 它会一直读取,直到发现自身位于下一个结束对象标记上为止。 它在下一个结束对象标记上停止,因为中间没有指示对象内的对象的起始对象标记。 如果要转换数组,需遵守有关开始标记和结束标记的相同规则。 有关示例,请参阅本文后面的 Stack<T> 示例转换器。

错误处理

序列化程序为 JsonException 和 NotSupportedException 异常类型提供特殊处理。

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")),则序列化程序仍会设置 Path、LineNumber 和 BytePositionInLine 属性。

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 路径信息。

注册自定义转换器

注册自定义转换器,使 Serialize 和 Deserialize 方法能够使用它。可以选择以下方式之一:

注册示例 – 转换器集

下面是一个示例,展示如何将 DateTimeOffsetJsonConverter 设置为 DateTimeOffset 类型属性的默认值:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
serializeOptions.Converters.Add(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 的代码无需配置 JsonSerializerOptions.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,则当值最初序列化为 ulong 或 BigInteger 时,这可能会导致超出范围问题。 如果数字最初序列化为 double,则将具有小数点的数字分析为 decimal 可能会损失精度。

对于需要类型推理的场景,以下代码展示了一种用于 object 属性的自定义转换器。 代码进行转换:

  • 将 true 和 false 转换为 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)
        {
            var runtimeType = objectToWrite.GetType();
            if (runtimeType == typeof(object))
            {
                writer.WriteStartObject();
                writer.WriteEndObject();
                return;
            }

            JsonSerializer.Serialize(writer, objectToWrite, runtimeType, 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 Run()
        {
            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 有 Employee 与 Customer 两个派生类。多态反序列化意味着指定目标类型为 Person,但在运行时按 JSON 的类型线索生成正确的派生类对象。线索可以是鉴别器属性,也可以是特定属性是否存在。对于前述 .NET 6 及更早版本的场景,需要用自定义转换器实现这种逻辑。

下面的代码演示一个基类、两个派生类和适用于它们的一个自定义转换器。 该转换器使用鉴别器属性执行多态反序列化。 类型鉴别器不在类定义中,而是在序列化过程中创建,在反序列化过程中进行读取。

重要

示例代码要求 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"
  }
]

前面示例中的转换器代码会手动读取和写入每个属性。 一种替代方法是调用 Deserialize 或 Serialize 以执行某些工作。 有关示例,请参阅此 StackOverflow 文章。

实现多态反序列化的替代方法

可以在 Read 方法中调用 Deserialize:

  • 创建 Utf8JsonReader 实例的克隆。
    Utf8JsonReader 是一个结构,因此只需要赋值语句。
  • 使用克隆来读取鉴别器标记。
  • 确定目标类型后,将原始 Utf8JsonReader 实例传给 Deserialize。之所以可行,是因为原读取器仍停留在对象起始标记处。

这种方法的缺点是不能传入将转换器注册到 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();
options.Converters.Add(new JsonConverterFactoryForStackOfT());

使用默认系统转换器

在某些情况下,你可能希望在自定义转换器中使用默认系统转换器。 为此,请从 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 异常。)

这种空值处理方式通过跳过额外的转换器调用来改善性能,也避免要求可空类型的转换器在每次 Read 和 Write 开始时检查 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 Run()
        {
            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.

保留引用

默认情况下,引用数据仅在每次调用Serialize或Deserialize时缓存。 要将引用从一个 Serialize/Deserialize 调用持久化到另一个调用,请将 ReferenceResolver 实例固定在 Serialize/Deserialize 的调用点。 下面的代码演示了此方案的一个示例:

  • 你为 Company 类型编写了一个自定义转换器。
  • 你不希望手动序列化 Supervisor 属性,该属性是一个 Employee。 你想要将任务给序列化程序,并且还希望保留已保存的引用。

下面是 Employee 和 Company 类:

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();

上述示例仅执行序列化,但可采用类似的方法来进行反序列化。

自定义转换器中的 ReferenceResolver 限制

使用 Preserve时,请注意,当序列化程序调用自定义转换器时,不会保留引用处理状态。 这意味着,如果具有类型属于在启用引用保留的情况下进行序列化或反序列化处理的对象图的自定义转换器,则转换器以及任何嵌套的序列化调用都无法访问当前 ReferenceResolver 实例。

其他自定义转换器示例

从 Newtonsoft.Json 迁移到 System.Text.Json 一文包含自定义转换器的其他示例。

单元测试文件夹在源代码中包含其他自定义转换器示例,例如:

如果需要创建修改现有内置转换器行为的转换器,则可以获取现有转换器的源代码作为自定义的起点。

其他资源

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容