Как написать настраиваемые преобразователи для сериализации JSON (маршалинг) в .NET

В этой статье показано, как создать настраиваемые преобразователи для классов сериализации JSON, предоставляемых в пространстве имен System.Text.Json. Общие сведения о System.Text.Json см. в статье Как сериализировать и десериализировать JSON в .NET.

Преобразователь — это класс, который преобразует объект или значение в формат JSON и обратно. Пространство имен System.Text.Json имеет встроенные преобразователи для большинства примитивных типов, которые соответствуют примитивам JavaScript. Вы можете создать пользовательские преобразователи, чтобы переопределить поведение по умолчанию встроенного преобразователя. Рассмотрим пример.

  • Возможно, вы хотите, чтобы значения DateTime были представлены в формате мм/дд/гггг. По умолчанию поддерживается стандарт ISO 8601-1:2019, включая профиль RFC 3339. Дополнительные сведения см. в разделе Поддержка DateTime и DateTimeOffset в System.Text.Json.
  • Может потребоваться сериализовать POCO в виде строки JSON, например с типом PhoneNumber .

Вы также можете написать пользовательские преобразователи для настройки или расширения System.Text.Json с помощью новых функций. Далее в этой статье описываются следующие сценарии:

Visual Basic не может использоваться для записи пользовательских преобразователей, но может вызывать преобразователи, реализованные в библиотеках C#. Дополнительные сведения см. в статье о поддержке Visual Basic.

Шаблоны настраиваемых преобразователей

Существует два шаблона для создания настраиваемого преобразователя: базовый шаблон и шаблон фабрики. Шаблон фабрики предназначен для преобразователей, обрабатывающих типы Enum или открытые универсальные шаблоны. Базовый шаблон предназначен для неуниверсальных и закрытых универсальных типов. Например, для преобразователей следующих типов требуется шаблон фабрики:

Ниже приведены некоторые примеры типов, которые могут быть обработаны базовым шаблоном:

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

Базовый шаблон создает класс, который может работать с одним типом. Шаблон фабрики создает класс, который во время выполнения определяет, какой конкретный тип необходим, и динамически выполняет создание соответствующего преобразователя.

Пример базового преобразователя

Следующий пример представляет собой преобразователь, который переопределяет сериализацию по умолчанию для существующего типа данных. Для свойств 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 возвращает true только для 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[] 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. Для записи JSON используйте передаваемое в метод значение Utf8JsonWriter.
  • Переопределяйте метод CanConvert только при необходимости. Реализация по умолчанию возвращает true, если тип для преобразования имеет тип T. Поэтому для преобразователей, поддерживающих только тип T, не требуется переопределять этот метод. Пример преобразователя, в котором необходимо переопределить этот метод, приведен в разделе Поддержка полиморфной десериализации, далее по тексту этой статьи.

Вы можете ссылаться на исходный код встроенных преобразователей в качестве эталонных реализаций для написания настраиваемых преобразователей.

Этапы следования шаблону фабрики

Ниже объясняются шаги по созданию преобразователя, используя шаблон фабрики:

  • Создайте класс, наследующий от класса JsonConverterFactory.
  • Переопределите метод CanConvert так, чтобы возвращать true, когда тип для преобразования может быть обработан преобразователем. Например, если преобразователь предназначен для List<T>, он может обрабатывать только List<int>, List<string> и List<DateTime>.
  • Переопределите CreateConverter метод, чтобы вернуть экземпляр класса преобразователя, который будет обрабатывать преобразуемый тип, предоставляемый во время выполнения.
  • Создайте класс преобразователя с помощью метода CreateConverter.

Шаблон фабрики необходим для открытых универсальных шаблонов, так как код для преобразования объекта в строку и обратно не совпадает для всех типов. Преобразователь для открытого универсального типа (например, List<T>) должен создать преобразователь для закрытого универсального типа (например, List<DateTime>) в фоновом режиме. Необходимо написать код для обработки каждого закрытого универсального типа, который может обрабатывать преобразователь.

Тип Enum похож на открытый универсальный тип: преобразователь для Enum должен создать преобразователь для определенного типа Enum (например, WeekdaysEnum) в фоновом режиме.

Использование Utf8JsonReader в методе Read

Если ваш преобразователь выполняет преобразование объекта JSON, Utf8JsonReader помещается на маркер начального объекта, когда запускается метод Read. Затем необходимо прочитать все маркеры в этом объекте и выйти из метода с помощью средства чтения, размещенного на соответствующем маркере конечного объекта. Если вы считываете за пределы объекта или останавливаетесь перед достижением соответствующего конечного маркера, вы получите 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 использовали его. Воспользуйтесь одним из перечисленных ниже подходов.

  • Добавьте экземпляр класса преобразователя в коллекцию JsonSerializerOptions.Converters.
  • Примените атрибут [JsonConverter] к свойствам, для которых требуется настраиваемый преобразователь.
  • Примените атрибут [JsonConverter] к классу или структуре, представляющей настраиваемый тип значения.

Пример регистрации — коллекция преобразователей

Ниже приведен пример, который делает 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 не нужно использовать 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 добавлен преобразователь.
  • Атрибут [JsonConverter] применяется к настраиваемому типу значения или POCO.

Если в коллекции зарегистрировано несколько пользовательских преобразователей для типа, используется первый преобразователь, который возвращает true для CanConvert.

Встроенный преобразователь выбирается только в том случае, если соответствующий настраиваемый преобразователь не зарегистрирован.

Примеры преобразователей для выполнения стандартных сценариев

В следующих разделах приведены примеры преобразователей, в которых рассматриваются распространенные сценарии, необрабатываемые встроенными функциями.

Пример преобразователя 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"
//}

В примере показан код преобразователя и WeatherForecast класс со свойствами object . Метод Main десериализирует строку JSON в WeatherForecast экземпляр, сначала без использования преобразователя, а затем с помощью преобразователя. Выходные данные консоли показывают, что без преобразователя тип среды выполнения для Date свойства имеет значениеJsonElement; с преобразователем тип среды выполнения .DateTime

В папке модульного теста в пространстве имен System.Text.Json.Serialization содержится больше примеров настраиваемых преобразователей, обрабатывающих десериализацию свойств object.

Поддержка полиморфной десериализации

.NET 7 обеспечивает поддержку полиморфной сериализации и десериализации. Однако в предыдущих версиях .NET была ограничена поддержка полиморфной сериализации и не поддерживает десериализацию. Если вы используете .NET 6 или более раннюю версию, десериализация требует пользовательского преобразователя.

Например, предположим, что имеется абстрактный базовый класс Person с производными классами Employee и Customer. Полиморфная десериализация означает, что во время разработки можно указать Person в качестве целевого объекта десериализации, а CustomerEmployee объекты в JSON правильно десериализируются во время выполнения. Во время десериализации необходимо найти признаки, которые определяют требуемый тип в JSON. В каждом сценарии доступны различные типы признаков. Например, может быть доступно свойство дискриминатора или придется полагаться на присутствие или отсутствие конкретного свойства. В текущем выпуске System.Text.Json не предоставлены атрибуты для указания способов обработки сценариев полиморфной десериализации, поэтому необходимо использовать настраиваемые преобразователи.

В следующем коде показан базовый класс, два производных класса и настраиваемый преобразователь для них. Преобразователь использует свойство дискриминатора для полиморфной десериализации. Дискриминатор типа не находится в определениях классов, но создается во время сериализации и считывается во время десериализации.

Important

В примере кода требуется, чтобы пары имен и значений объекта 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.

Альтернативный способ выполнения полиморфной десериализации

Можно вызвать Deserialize в методе Read :

  • Создайте клон экземпляра Utf8JsonReader . Так как Utf8JsonReader это структура, это просто требует инструкции назначения.
  • Используйте клон для чтения через дискриминационные маркеры.
  • Вызовите Deserialize, используя исходный экземпляр Reader, после того как вам станет известен нужный тип. Можно вызвать Deserialize, поскольку исходный Reader экземпляр по-прежнему расположен для чтения токена начального объекта.

Недостатком этого метода является то, что вы не можете передать исходный экземпляр параметров, где регистрируется преобразователь Deserialize. Это приведет к переполнению стека, как описано в обязательных свойствах. В следующем примере показан метод, использующий эту альтернативу Read :

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

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

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

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

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

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

Поддержка кругового пути для Stack типов

Если десериализовать строку JSON в объект Stack, а затем сериализовать этот объект, содержимое стека будет организовано в обратном порядке. Это поведение применяется к следующим типам и интерфейсам, а также определяемым пользователем типам, производным от них:

Чтобы включить поддержку сериализации и десериализации, сохраняющей исходный порядок в стеке, требуется пользовательский преобразователь.

В следующем коде показан пользовательский преобразователь, включающий поддержку кругового пути для объектов Stack<T>:

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

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

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

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

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

            return converter;
        }
    }

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

            var elements = new Stack<T>();

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

                reader.Read();
            }

            return elements;
        }

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

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

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

            writer.WriteEndArray();
        }
    }
}

В следующем коде регистрируется преобразователь.

var options = new JsonSerializerOptions();
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.)

Это поведение обработки значений NULL в основном предназначено для оптимизации производительности путем пропуска дополнительного вызова преобразователя. Кроме того, это позволяет избежать необходимости принудительного выполнения преобразователей для типов, допускающих значение null, чтобы проверять наличие null в начале каждого переопределения метода Read и Write.

Чтобы разрешить пользовательскому преобразователю обработку 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 приведены дополнительные примеры настраиваемых преобразователей.

В папке модульных тестов в исходном коде System.Text.Json.Serialization есть и другие примеры настраиваемых преобразователей, например:

Если необходимо создать преобразователь, изменяющий поведение существующего встроенного преобразователя, можно получить исходный код существующего преобразователя в качестве отправной точки для настройки.

Дополнительные ресурсы