Как написать настраиваемые преобразователи для сериализации 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
с помощью новых функций. Далее в этой статье описываются следующие сценарии:
- Десериализация выводимых типов в свойства объекта.
- Поддержка полиморфной десериализации.
- Поддержка кругового пути для
Stack
типов. - Используйте системный преобразователь по умолчанию.
Visual Basic не может использоваться для записи пользовательских преобразователей, но может вызывать преобразователи, реализованные в библиотеках C#. Дополнительные сведения см. в статье о поддержке Visual Basic.
Шаблоны настраиваемых преобразователей
Существует два шаблона для создания настраиваемого преобразователя: базовый шаблон и шаблон фабрики. Шаблон фабрики предназначен для преобразователей, обрабатывающих типы Enum
или открытые универсальные шаблоны. Базовый шаблон предназначен для неуниверсальных и закрытых универсальных типов. Например, для преобразователей следующих типов требуется шаблон фабрики:
Ниже приведены некоторые примеры типов, которые могут быть обработаны базовым шаблоном:
Базовый шаблон создает класс, который может работать с одним типом. Шаблон фабрики создает класс, который в среде выполнения определяет, какой конкретный тип требуется, и динамически создает соответствующий преобразователь.
Пример базового преобразователя
Следующий пример представляет собой преобразователь, который переопределяет сериализацию по умолчанию для существующего типа данных. Для свойств 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")
), сериализатор по-прежнему задает PathLineNumberсвойства и 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.
Если в коллекции зарегистрировано Converters
несколько пользовательских преобразователей для типа, используется первый преобразователь, возвращающийся true
для CanConvert
.
Встроенный преобразователь выбирается только в том случае, если соответствующий настраиваемый преобразователь не зарегистрирован.
Примеры преобразователей для выполнения стандартных сценариев
В следующих разделах приведены примеры преобразователей, в которых рассматриваются распространенные сценарии, необрабатываемые встроенными функциями.
- Десериализация выводимых типов в свойства объекта.
- Поддержка кругового пути для
Stack
типов. - Используйте системный преобразователь по умолчанию.
Пример преобразователя см. в разделе "Поддерживаемые DataTable типы коллекций".
Десериализация выводимых типов в свойства объекта
При десериализации в свойство типа object
создается объект JsonElement
. Причина заключается в том, что десериализатор не знает, какой тип среды выполнения создать, и не пытается угадать. Например, если свойство 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) =>
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"
//}
В примере показан код преобразователя и WeatherForecast
класс со свойствами object
. Метод Main
десериализирует строку JSON в WeatherForecast
экземпляр, сначала без использования преобразователя, а затем с помощью преобразователя. Выходные данные консоли показывают, что без преобразователя тип времени выполнения для Date
свойства имеет значение JsonElement
; с преобразователем — тип DateTime
времени выполнения.
В папке модульного теста в пространстве имен System.Text.Json.Serialization
содержится больше примеров настраиваемых преобразователей, обрабатывающих десериализацию свойств object
.
Поддержка полиморфной десериализации
.NET 7 обеспечивает поддержку полиморфной сериализации и десериализации. Однако в предыдущих версиях .NET была ограничена поддержка полиморфной сериализации и не поддерживает десериализацию. Если вы используете .NET 6 или более раннюю версию, десериализация требует пользовательского преобразователя.
Например, предположим, что имеется абстрактный базовый класс Person
с производными классами Employee
и Customer
. Полиморфная десериализации означает, что во время разработки можно указать Person
в качестве цели десериализации, а объекты Customer
и Employee
в JSON правильно десериализованы во время выполнения. Во время десериализации необходимо найти признаки, которые определяют требуемый тип в 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"
}
]
Код преобразователя в предыдущем примере считывает и записывает каждое свойство вручную. Альтернативой является вызов 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 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
вызова на другой, корень 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();
В предыдущем примере сериализация выполняется только при сериализации, но аналогичный подход можно применить для десериализации.
Другие примеры настраиваемых преобразователей
В статье о миграции из Newtonsoft.Json в System.Text.Json приведены дополнительные примеры настраиваемых преобразователей.
В папке модульных тестов в исходном коде System.Text.Json.Serialization
есть и другие примеры настраиваемых преобразователей, например:
- Преобразователь Int32, который преобразовывает значение NULL в 0 при десериализации.
- Преобразователь Int32, который допускает как строковые, так и числовые значения при десериализации.
- Преобразователь Enum.
- Вывод списка<преобразователя T> , который принимает внешние данные
- Преобразователь Long[], который работает с разделенным запятыми списком чисел
Если необходимо создать преобразователь, изменяющий поведение существующего встроенного преобразователя, можно получить исходный код существующего преобразователя в качестве отправной точки для настройки.