如何在 .NET 中编写用于 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 支持
  • 支持自定义值类型。 例如,PhoneNumber 结构。

还可以编写自定义转换器,以使用当前版本中未包含的功能自定义或扩展 System.Text.Json。 本文后面部分介绍了以下方案:

在为自定义转换器编写的代码中,请注意,使用新的 JsonSerializerOptions 实例会带来重大性能损失。 有关详细信息,请参阅重用 JsonSerializerOptions 实例

Visual Basic 不能用于编写自定义转换器,但可调用在 C# 库中实现的转换器。 有关详细信息,请参阅 Visual Basic 支持

自定义转换器模式

用于创建自定义转换器的模式有两种:基本模式和工厂模式。 工厂模式适用于处理类型 Enum 或开放式泛型的转换器。 基本模式适用于非泛型或封闭式泛型类型。 例如,适用于以下类型的转换器需要工厂模式:

可以通过基本模式处理的类型的一些示例包括:

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

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

示例基本转换器

下面的示例是一个转换器,可重写现有数据类型的默认序列化。 该转换器将 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> 的自定义转换器。 该代码遵循工厂模式,因为第一个泛型类型参数是 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 keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { 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)
                {
                    var 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 方法可使用它。 选择以下方法之一:

注册示例 - 转换器集合

对于 DateTimeOffset 类型的属性,以下示例将 DateTimeOffsetJsonConverter 设置为默认值:

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 属性的反序列化的自定义转换器的更多示例。

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

namespace SystemTextJsonSamples
{
    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) =>
            throw new InvalidOperationException("Should not get here.");
    }
}

下面的代码注册转换器:

var deserializeOptions = new JsonSerializerOptions
{
    Converters =
    {
        new ObjectToInferredTypesConverter()
    }
};

下面是一种具有 object 属性的示例类型:

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

以下要反序列化的 JSON 示例包含将作为 DateTimelongstring 进行反序列化的值:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
}

如果没有自定义转换器,则反序列化会将 JsonElement 放入每个属性中。

System.Text.Json.Serialization 命名空间中的单元测试文件夹包含处理到 object 属性的反序列化的自定义转换器的更多示例。

支持包含非字符串键的字典

对字典集合的内置支持适用于 Dictionary<string, TValue>。 即,键必须是字符串。 若要支持将整数或某种其他类型用作键的字典,需要自定义转换器。

下面的代码演示一个处理 Dictionary<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 keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { 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)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

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

                writer.WriteEndObject();
            }
        }
    }
}

下面的代码注册转换器:

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

该转换器可以序列化和反序列化使用以下 Enum 的以下类的 TemperatureRanges 属性:

public class WeatherForecastWithEnumDictionary
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
    public Dictionary<SummaryWordsEnum, int>? TemperatureRanges { get; set; }
}

public enum SummaryWordsEnum
{
    Cold, Hot
}

来自序列化的 JSON 输出类似于以下示例:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "TemperatureRanges": {
    "Cold": 20,
    "Hot": 40
  }
}

System.Text.Json.Serialization 命名空间中的单元测试文件夹包含处理非字符串键字典的自定义转换器的更多示例。

支持多态反序列化

.NET 7 支持多态序列化和反序列化。 在以前的 .NET 版本中,对多态序列化的支持有限,且不支持反序列化。 如果使用 .NET 6 或更低版本,则反序列化需要使用自定义转换器。

例如,假设有一个 Person 抽象基类,其中包含 EmployeeCustomer 派生类。 多态反序列化意味着可以在设计时将 Person 指定为反序列化目标,JSON 中的 CustomerEmployee 对象会在运行时正确地进行反序列化。 在反序列化过程中,必须查找标识 JSON 中所需类型的线索。 可用的线索类型因各个方案而异。 例如,可以使用鉴别器属性,或者可能必须依赖于特定属性是否存在。 System.Text.Json 的当前版本不提供属性来指定如何处理多态反序列化方案,因此需要自定义转换器。

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

重要

示例代码要求 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 是一个结构,因此只需要赋值语句。
  • 使用克隆来读取 discriminator 标记。
  • 知道所需的类型后,使用原始 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;
}

支持堆栈的往返<T>

如果将 JSON 字符串反序列化为 Stack<T> 对象,然后再序列化该对象,则堆栈的内容将按相反的顺序排列。 此行为适用于以下类型和接口以及从它们派生的用户定义类型:

若要支持在堆栈中保留原始顺序的序列化和反序列化,则需要自定义转换器。

下面的代码演示了一个自定义转换器,用于实现与 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

使用默认系统转换器

在某些情况下,你可能希望在自定义转换器中使用默认系统转换器。 为此,请从 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 处理行为主要用于,通过跳过对转换器的额外调用来优化性能。 此外,它可避免在每个 ReadWrite 方法重写开始时强制可以为 null 的类型的转换器检查 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.

保留引用

默认情况下,每次调用 或 时,仅缓存引用 Serialize 数据 Deserialize。 若要保留从一个 Serialize/Deserialize 调用到另一个调用的引用,请在 Serialize/Deserialize 的调用站点中将 ReferenceResolver 实例放在根目录。 下面的代码演示了此方案的一个示例:

  • 你为 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();

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

若要了解如何保留引用,请参阅此页面的 .NET 5 版本

其他自定义转换器示例

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

System.Text.Json.Serialization 源代码中的单元测试文件夹包含其他自定义转换器示例,例如:

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

其他资源