如何在 .NET 中编写用于 JSON 序列化(封送)的自定义转换器How to write custom converters for JSON serialization (marshalling) in .NET

本文介绍如何为 System.Text.Json 命名空间中提供的 JSON 序列化类创建自定义转换器。This article shows how to create custom converters for the JSON serialization classes that are provided in the System.Text.Json namespace. 有关 System.Text.Json 简介,请参阅如何在 .NET 中对 JSON 数据进行序列化和反序列化For an introduction to System.Text.Json, see How to serialize and deserialize JSON in .NET.

转换器是一种将对象或值与 JSON 相互转换的类。A converter is a class that converts an object or a value to and from JSON. System.Text.Json 命名空间为映射到 JavaScript 基元的大多数基元类型提供内置转换器。The System.Text.Json namespace has built-in converters for most primitive types that map to JavaScript primitives. 可以编写自定义转换器来实现以下目标:You can write custom converters:

  • 重写内置转换器的默认行为。To override the default behavior of a built-in converter. 例如,可能希望用 mm/dd/yyyy 格式而不是默认 ISO 8601-1:2019 格式来表示 DateTime 值。For example, you might want DateTime values to be represented by mm/dd/yyyy format instead of the default ISO 8601-1:2019 format.
  • 支持自定义值类型。To support a custom value type. 例如,PhoneNumber 结构。For example, a PhoneNumber struct.

还可以编写自定义转换器,以使用当前版本中未包含的功能自定义或扩展 System.Text.JsonYou can also write custom converters to customize or extend System.Text.Json with functionality not included in the current release. 本文后面部分介绍了以下方案:The following scenarios are covered later in this article:

自定义转换器模式Custom converter patterns

用于创建自定义转换器的模式有两种:基本模式和工厂模式。There are two patterns for creating a custom converter: the basic pattern and the factory pattern. 工厂模式适用于处理类型 Enum 或开放式泛型的转换器。The factory pattern is for converters that handle type Enum or open generics. 基本模式适用于非泛型或封闭式泛型类型。The basic pattern is for non-generic and closed generic types. 例如,适用于以下类型的转换器需要工厂模式:For example, converters for the following types require the factory pattern:

  • Dictionary<TKey, TValue>
  • Enum
  • List<T>

可以通过基本模式处理的类型的一些示例包括:Some examples of types that can be handled by the basic pattern include:

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

基本模式创建的类可以处理一种类型。The basic pattern creates a class that can handle one type. 工厂模式创建的类在运行时确定所需的特定类型,并动态创建适当的转换器。The factory pattern creates a class that determines, at run time, which specific type is required and dynamically creates the appropriate converter.

示例基本转换器Sample basic converter

下面的示例是一个转换器,可重写现有数据类型的默认序列化。The following sample is a converter that overrides default serialization for an existing data type. 该转换器将 mm/dd/yyyy 格式用于 DateTimeOffset 属性。The converter uses mm/dd/yyyy format for DateTimeOffset properties.

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

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetConverter : 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));
    }
}

示例工厂模式转换器Sample factory pattern converter

下面的代码演示一个处理 Dictionary<Enum,TValue> 的自定义转换器。The following code shows a custom converter that works with Dictionary<Enum,TValue>. 该代码遵循工厂模式,因为第一个泛型类型参数是 Enum,第二个参数是开放参数。The code follows the factory pattern because the first generic type parameter is Enum and the second is open. CanConvert 方法仅对具有两个泛型参数的 Dictionary 返回 true,其中第一个参数是 Enum 类型。The CanConvert method returns true only for a Dictionary with two generic parameters, the first of which is an Enum type. 内部转换器获取现有转换器,以处理在运行时为 TValue 提供的任何类型。The inner converter gets an existing converter to handle whichever type is provided at run time for TValue.

using System;
using System.Collections.Generic;
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 Type _keyType;
            private Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _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();
                }

                Dictionary<TKey, TValue> 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.
                    TValue v;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        v = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        v = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

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

                throw new JsonException();
            }

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

                foreach (KeyValuePair<TKey, TValue> kvp in dictionary)
                {
                    writer.WritePropertyName(kvp.Key.ToString());

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, kvp.Value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, kvp.Value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

前面的代码与本文后面的支持包含非字符串键的字典中演示的代码相同。The preceding code is the same as what is shown in the Support Dictionary with non-string key later in this article.

遵循基本模式的步骤Steps to follow the basic pattern

以下步骤说明如何遵循基本模式来创建转换器:The following steps explain how to create a converter by following the basic pattern:

  • 创建一个派生自 JsonConverter<T> 的类,其中 T 是要进行序列化和反序列化的类型。Create a class that derives from JsonConverter<T> where T is the type to be serialized and deserialized.
  • 重写 Read 方法,以反序列化传入 JSON 并将其转换为类型 TOverride the Read method to deserialize the incoming JSON and convert it to type T. 使用传递给方法的 Utf8JsonReader 读取 JSON。Use the Utf8JsonReader that is passed to the method to read the JSON.
  • 重写 Write 方法以序列化 T 类型的传入对象。Override the Write method to serialize the incoming object of type T. 使用传递给方法的 Utf8JsonWriter 写入 JSON。Use the Utf8JsonWriter that is passed to the method to write the JSON.
  • 仅当需要时才重写 CanConvert 方法。Override the CanConvert method only if necessary. 当要转换的类型属于类型 T 时,默认实现会返回 trueThe default implementation returns true when the type to convert is of type T. 因此,仅支持类型 T 的转换器不需要重写此方法。Therefore, converters that support only type T don't need to override this method. 有关的确需要重写此方法的转换器的示例,请参阅本文后面的多态反序列化部分。For an example of a converter that does need to override this method, see the polymorphic deserialization section later in this article.

可以参阅内置转换器源代码作为用于编写自定义转换器的参考实现。You can refer to the built-in converters source code as reference implementations for writing custom converters.

遵循工厂模式的步骤Steps to follow the factory pattern

以下步骤说明如何遵循工厂模式来创建转换器:The following steps explain how to create a converter by following the factory pattern:

  • 创建一个从 JsonConverterFactory 派生的类。Create a class that derives from JsonConverterFactory.
  • 重写 CanConvert 方法,以在要转换的类型是转换器可处理的类型时返回 true。Override the CanConvert method to return true when the type to convert is one that the converter can handle. 例如,如果转换器适用于 List<T>,则它可能仅处理 List<int>List<string>List<DateTime>For example, if the converter is for List<T> it might only handle List<int>, List<string>, and List<DateTime>.
  • 重写 CreateConverter 方法,以返回将处理在运行时提供的要转换的类型的转换器类实例。Override the CreateConverter method to return an instance of a converter class that will handle the type-to-convert that is provided at run time.
  • 创建 CreateConverter 方法实例化的转换器类。Create the converter class that the CreateConverter method instantiates.

开放式泛型需要工厂模式,因为用于将对象与字符串相互转换的代码对于所有类型并不相同。The factory pattern is required for open generics because the code to convert an object to and from a string isn't the same for all types. 适用于开放式泛型类型(例如 List<T>)的转换器必须在幕后为封闭式泛型类型(例如 List<DateTime>)创建转换器。A converter for an open generic type (List<T>, for example) has to create a converter for a closed generic type (List<DateTime>, for example) behind the scenes. 必须编写代码来处理转换器可处理的每种封闭式泛型类型。Code must be written to handle each closed-generic type that the converter can handle.

Enum 类型类似于开放式泛型类型:适用于 Enum 的转换器必须在幕后为特定 Enum(例如WeekdaysEnum)创建转换器。The Enum type is similar to an open generic type: a converter for Enum has to create a converter for a specific Enum (WeekdaysEnum, for example) behind the scenes.

错误处理Error handling

如果需要在错误处理代码中引发异常,请考虑在不使用消息的情况下引发 JsonExceptionIf you need to throw an exception in error-handling code, consider throwing a JsonException without a message. 此异常类型会自动创建消息,其中包括导致错误的 JSON 部分的路径。This exception type automatically creates a message that includes the path to the part of the JSON that caused the error. 例如,语句 throw new JsonException(); 会生成如以下示例的错误消息:For example, the statement throw new JsonException(); produces an error message like the following example:

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 属性中提供路径。If you do provide a message (for example, throw new JsonException("Error occurred"), the exception still provides the path in the Path property.

注册自定义转换器Register a custom converter

注册自定义转换器,使 SerializeDeserialize 方法可使用它。Register a custom converter to make the Serialize and Deserialize methods use it. 选择以下方法之一:Choose one of the following approaches:

注册示例 - 转换器集合Registration sample - Converters collection

下面是一个示例,该示例将 DateTimeOffsetConverter 设为类型 DateTimeOffset 的属性的默认值:Here's an example that makes the DateTimeOffsetConverter the default for properties of type DateTimeOffset:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new DateTimeOffsetConverter());
serializeOptions.WriteIndented = true;
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

假设序列化以下类型的实例:Suppose you serialize an instance of the following type:

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

下面是演示使用自定义转换器的 JSON 输出的示例:Here's an example of JSON output that shows the custom converter was used:

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

下面的代码使用的方法与使用自定义 DateTimeOffset 转换器进行反序列化相同:The following code uses the same approach to deserialize using the custom DateTimeOffset converter:

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

注册示例 - 属性上的 [JsonConverter]Registration sample - [JsonConverter] on a property

下面的代码为 Date 属性选择自定义转换器:The following code selects a custom converter for the Date property:

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

用于序列化 WeatherForecastWithConverterAttribute 的代码不需要使用 JsonSerializeOptions.ConvertersThe code to serialize WeatherForecastWithConverterAttribute doesn't require the use of JsonSerializeOptions.Converters:

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

用于反序列化的代码也不需要使用 ConvertersThe code to deserialize also doesn't require the use of Converters:

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

注册示例 - 类型上的 [JsonConverter]Registration sample - [JsonConverter] on a type

下面的代码创建一个结构并向它应用 [JsonConverter] 属性:Here's code that creates a struct and applies the [JsonConverter] attribute to it:

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            _degrees = degrees;
            _isCelsius = celsius;
        }
        private bool _isCelsius;
        private int _degrees;
        public int Degrees => _degrees;
        public bool IsCelsius => _isCelsius;
        public bool IsFahrenheit => !_isCelsius;
        public override string ToString() =>
            $"{_degrees.ToString()}{(_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);
        }
    }
}

下面是适用于上述结构的自定义转换器:Here's the custom converter for the preceding struct:

using System;
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());
    }
}

结构上的 [JsonConvert] 属性将自定义转换器注册为类型 Temperature 的属性的默认值。The [JsonConvert] attribute on the struct registers the custom converter as the default for properties of type Temperature. 进行序列化或反序列化时,转换器会自动用于以下类型的 TemperatureCelsius 属性:The converter is automatically used on the TemperatureCelsius property of the following type when you serialize or deserialize it:

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

转换器注册优先级Converter registration precedence

在序列化或反序列化过程中,按以下顺序(从最高优先级到最低优先级来列出)为每个 JSON 元素选择转换器:During serialization or deserialization, a converter is chosen for each JSON element in the following order, listed from highest priority to lowest:

  • 应用于属性的 [JsonConverter][JsonConverter] applied to a property.
  • Converters 集合添加的转换器。A converter added to the Converters collection.
  • 应用于自定义值类型或 POCO 的 [JsonConverter][JsonConverter] applied to a custom value type or POCO.

如果在 Converters 集合中注册了适用于某种类型的多个自定义转换器,则使用第一个为 CanConvert 返回 true 的转换器。If multiple custom converters for a type are registered in the Converters collection, the first converter that returns true for CanConvert is used.

仅当未注册适用自定义转换器时,才会选择内置转换器。A built-in converter is chosen only if no applicable custom converter is registered.

常见方案的转换器示例Converter samples for common scenarios

以下各部分提供的转换器示例用于解决内置功能不处理的一些常见方案。The following sections provide converter samples that address some common scenarios that built-in functionality doesn't handle.

将推断类型反序列化为对象属性Deserialize inferred types to object properties

反序列化为类型 object 的属性时,将创建一个 JsonElement 对象。When deserializing to a property of type object, a JsonElement object is created. 这是因为反序列化程序不知道要创建的 CLR 类型,也不会尝试进行猜测。The reason is that the deserializer doesn't know what CLR type to create, and it doesn't try to guess. 例如,如果 JSON 属性具有“true”,则反序列化程序不会推断值为 Boolean,如果元素具有“01/01/2019”,则反序列化程序不会推断它是 DateTimeFor example, if a JSON property has "true", the deserializer doesn't infer that the value is a Boolean, and if an element has "01/01/2019", the deserializer doesn't infer that it's a DateTime.

类型推理可能不准确。Type inference can be inaccurate. 如果反序列化程序将没有小数点的 JSON 数字分析为 long,则当值最初序列化为 ulongBigInteger 时,这可能会导致超出范围问题。If the deserializer parses a JSON number that has no decimal point as a long, that might result in out-of-range issues if the value was originally serialized as a ulong or BigInteger. 如果数字最初序列化为 decimal,则将具有小数点的数字分析为 double 可能会损失精度。Parsing a number that has a decimal point as a double might lose precision if the number was originally serialized as a decimal.

对于需要类型推理的方案,以下代码演示适用于 object 属性的自定义转换器。For scenarios that require type inference, the following code shows a custom converter for object properties. 代码:The code converts:

  • truefalse 转换为 Booleantrue and false to Boolean
  • 将不带小数的数字转换为 longNumbers without a decimal to long
  • 将带有小数的数字转换为 doubleNumbers with a decimal to double
  • 将日期转换为 DateTimeDates to DateTime
  • 将字符串转换为 stringStrings to string
  • 将其他所有内容转换为 JsonElementEverything else to JsonElement
using System;
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)
        {
            if (reader.TokenType == JsonTokenType.True)
            {
                return true;
            }

            if (reader.TokenType == JsonTokenType.False)
            {
                return false;
            }

            if (reader.TokenType == JsonTokenType.Number)
            {
                if (reader.TryGetInt64(out long l))
                {
                    return l;
                }

                return reader.GetDouble();
            }

            if (reader.TokenType == JsonTokenType.String)
            {
                if (reader.TryGetDateTime(out DateTime datetime))
                {
                    return datetime;
                }

                return reader.GetString();
            }

            // Use JsonElement as fallback.
            // Newtonsoft uses JArray or JObject.
            using JsonDocument document = JsonDocument.ParseValue(ref reader);
            return document.RootElement.Clone();
        }

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
                throw new InvalidOperationException("Should not get here.");
    }
}

下面的代码注册转换器:The following code registers the converter:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new ObjectToInferredTypesConverter());

下面是一种具有 object 属性的示例类型:Here's an example type with object properties:

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

以下要反序列化的 JSON 示例包含将作为 DateTimelongstring 进行反序列化的值:The following example of JSON to deserialize contains values that will be deserialized as DateTime, long, and string:

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

如果没有自定义转换器,则反序列化会将 JsonElement 放入每个属性中。Without the custom converter, deserialization puts a JsonElement in each property.

System.Text.Json.Serialization 命名空间中的单元测试文件夹包含处理到 object 属性的反序列化的自定义转换器的更多示例。The unit tests folder in the System.Text.Json.Serialization namespace has more examples of custom converters that handle deserialization to object properties.

支持包含非字符串键的字典Support Dictionary with non-string key

对字典集合的内置支持适用于 Dictionary<string, TValue>The built-in support for dictionary collections is for Dictionary<string, TValue>. 即,键必须是字符串。That is, the key must be a string. 若要支持将整数或某种其他类型用作键的字典,需要自定义转换器。To support a dictionary with an integer or some other type as the key, a custom converter is required.

下面的代码演示一个处理 Dictionary<Enum,TValue> 的自定义转换器:The following code shows a custom converter that works with Dictionary<Enum,TValue>:

using System;
using System.Collections.Generic;
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 Type _keyType;
            private Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _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();
                }

                Dictionary<TKey, TValue> 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.
                    TValue v;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        v = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        v = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

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

                throw new JsonException();
            }

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

                foreach (KeyValuePair<TKey, TValue> kvp in dictionary)
                {
                    writer.WritePropertyName(kvp.Key.ToString());

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, kvp.Value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, kvp.Value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

下面的代码注册转换器:The following code registers the converter:

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

该转换器可以序列化和反序列化使用以下 Enum 的以下类的 TemperatureRanges 属性:The converter can serialize and deserialize the TemperatureRanges property of the following class that uses the following Enum:

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 输出类似于以下示例:The JSON output from serialization looks like the following example:

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

System.Text.Json.Serialization 命名空间中的单元测试文件夹包含处理非字符串键字典的自定义转换器的更多示例。The unit tests folder in the System.Text.Json.Serialization namespace has more examples of custom converters that handle non-string-key dictionaries.

支持多态反序列化Support polymorphic deserialization

内置功能提供有限范围的多态序列化,但完全不支持反序列化。Built-in features provide a limited range of polymorphic serialization but no support for deserialization at all. 反序列化需要自定义转换器。Deserialization requires a custom converter.

例如,假设有一个 Person 抽象基类,其中包含 EmployeeCustomer 派生类。Suppose, for example, you have a Person abstract base class, with Employee and Customer derived classes. 多态反序列化意味着可以在设计时将 Person 指定为反序列化目标,JSON 中的 CustomerEmployee 对象会在运行时正确地进行反序列化。Polymorphic deserialization means that at design time you can specify Person as the deserialization target, and Customer and Employee objects in the JSON are correctly deserialized at run time. 在反序列化过程中,必须查找标识 JSON 中所需类型的线索。During deserialization, you have to find clues that identify the required type in the JSON. 可用的线索类型因各个方案而异。The kinds of clues available vary with each scenario. 例如,可以使用鉴别器属性,或者可能必须依赖于特定属性是否存在。For example, a discriminator property might be available or you might have to rely on the presence or absence of a particular property. System.Text.Json 的当前版本不提供属性来指定如何处理多态反序列化方案,因此需要自定义转换器。The current release of System.Text.Json doesn't provide attributes to specify how to handle polymorphic deserialization scenarios, so custom converters are required.

下面的代码演示一个基类、两个派生类和适用于它们的一个自定义转换器。The following code shows a base class, two derived classes, and a custom converter for them. 该转换器使用鉴别器属性执行多态反序列化。The converter uses a discriminator property to do polymorphic deserialization. 类型鉴别器不在类定义中,而是在序列化过程中创建,在反序列化过程中进行读取。The type discriminator isn't in the class definitions but is created during serialization and is read during deserialization.

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

下面的代码注册转换器:The following code registers the converter:

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

该转换器可以反序列化通过用于序列化的相同转换器而创建的 JSON,例如:The converter can deserialize JSON that was created by using the same converter to serialize, for example:

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

前面示例中的转换器代码会手动读取和写入每个属性。The converter code in the preceding example reads and writes each property manually. 一种替代方法是调用 DeserializeSerialize 以执行某些工作。An alternative is to call Deserialize or Serialize to do some of the work. 有关示例,请参阅此 StackOverflow 文章For an example, see this StackOverflow post.

支持堆栈的往返<T>Support round trip for Stack<T>

如果将 JSON 字符串反序列化为 Stack<T> 对象,然后再序列化该对象,则堆栈的内容将按相反的顺序排列。If you deserialize a JSON string into a Stack<T> object and then serialize that object, the contents of the stack are in reverse order. 此行为适用于以下类型和接口以及从它们派生的用户定义类型:This behavior applies to the following types and interface, and user-defined types that derive from them:

若要支持在堆栈中保留原始顺序的序列化和反序列化,则需要自定义转换器。To support serialization and deserialization that retains the original order in the stack, a custom converter is required.

下面的代码演示了一个自定义转换器,用于实现与 Stack<T> 对象之间的来回转换:The following code shows a custom converter that enables round-tripping to and from Stack<T> objects:

using System;
using System.Collections.Generic;
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)
        {
            return 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 || !reader.Read())
            {
                throw new JsonException();
            }

            var elements = new Stack<T>();

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

                if (!reader.Read())
                {
                    throw new JsonException();
                }
            }

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

下面的代码注册转换器:The following code registers the converter:

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

处理 NULL 值Handle null values

默认情况下,序列化程序处理 null 值,如下所示:By default, the serializer handles null values as follows:

  • 对于引用类型和 Nullable<T> 类型:For reference types and Nullable<T> types:

    • 它在序列化时不会将 null 传递到自定义转换器。It does not pass null to custom converters on serialization.
    • 它在反序列化时不会将 JsonTokenType.Null 传递到自定义转换器。It does not pass JsonTokenType.Null to custom converters on deserialization.
    • 它在反序列化时返回 null 实例。It returns a null instance on deserialization.
    • 它在序列化时直接使用编写器写入 nullIt writes null directly with the writer on serialization.
  • 对于不可为 null 的值类型:For non-nullable value types:

    • 它在反序列化时将 JsonTokenType.Null 传递到自定义转换器。It passes JsonTokenType.Null to custom converters on deserialization. (如果没有可用的自定义转换器,则由该类型的内部转换器引发 JsonException 异常。)(If no custom converter is available, a JsonException exception is thrown by the internal converter for the type.)

此 null 处理行为主要用于,通过跳过对转换器的额外调用来优化性能。This null-handling behavior is primarily to optimize performance by skipping an extra call to the converter. 此外,它可避免在每个 ReadWrite 方法重写开始时强制可以为 null 的类型的转换器检查 nullIn addition, it avoids forcing converters for nullable types to check for null at the start of every Read and Write method override.

若要启用自定义转换器来处理引用或值类型的 null,请重写 JsonConverter<T>.HandleNull 以返回 true,如以下示例中所示:To enable a custom converter to handle null for a reference or value type, override JsonConverter<T>.HandleNull to return true, as shown in the following example:

using System;
using System.Collections.Generic;
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)
        {
            string val = reader.GetString();
            return val ?? "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.

其他自定义转换器示例Other custom converter samples

从 Newtonsoft.Json 迁移到 System.Text.Json 一文包含自定义转换器的其他示例。The Migrate from Newtonsoft.Json to System.Text.Json article contains additional samples of custom converters.

System.Text.Json.Serialization 源代码中的单元测试文件夹包含其他自定义转换器示例,例如:The unit tests folder in the System.Text.Json.Serialization source code includes other custom converter samples, such as:

如果需要创建修改现有内置转换器行为的转换器,则可以获取现有转换器的源代码作为自定义的起点。If you need to make a converter that modifies the behavior of an existing built-in converter, you can get the source code of the existing converter to serve as a starting point for customization.

其他资源Additional resources