В подавляющем большинстве случаев, сериализатор из System.Text.Json вполне справляется с сериализацией и десериализацией объектов практически любой сложности. Однако, встречаются моменты, когда даже с множество настроек из JsonOptions
не справляются с десериализацией объекта. В этом случае, мы можем написать свои пользовательские конвертеры для класса JsonSerializer
.
Пример
Рассмотрим следующий JSON-объект:
{ "registration_date": "2002-07-25T20:00:00.000000Z", "updated_at": "2023-05-10T08:26:21+0300" }
в этом объекте представлены две даты — регистрации и обновления какой-то сущности. Обе даты соответствуют форматам ISO 8601, поддержка которого заявлена разработчиками System.Text.Json. Но, при попытке десериализовать такой объект мы получим ошибку:
using System; using System.Text.Json; namespace ConsoleApp3 { public class Dt { [JsonPropertyName("registration_date")] public DateTime RegDate { get; set; } [JsonPropertyName("updated_at")] public DateTime Updated { get; set; } } internal class Program { static void Main(string[] args) { string json = @"{""registration_date"": ""2002-07-25T20:00:00.000000Z"", ""updated_at"": ""2023-05-10T08:26:21+0300""}"; var dt = JsonSerializer.Deserialize<Dt>(json); Console.WriteLine(dt.RegDate); Console.WriteLine(dt.Updated); } } }
Эта ошибка возникает пр попытке десериализовать дату из «updated_at». И теперь перед нами стоит задача — как десериализовать оба поля JSON-объекта, каждое из которых является датой, но имеют разный формат? Причем, один из форматов не поддерживается стандартными средствами JsonSerializer
. В этом случае нам необходимо будет написать свой пользовательский конвертер JSON.
Пользовательские конвертеры JSON
Пользовательский конвертер JSON — это обычный класс C#, который наследует универсальный класс JsonConverter<T>
где T
— это тип сериализуемого/десериализуемого свойства. Этот класс должен переопределять два метода: Read()
— для чтения значения свойства из JSON и Write()
для записи свойства в JSON. То есть, в нашем случае, пользовательский конвертер JSON для DateTime
должен выглядеть следующим образом:
public class JsonDateTimeConverter : JsonConverter<DateTime> { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { //тут код чтения даты из JSON } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { //тут код записи даты в JSON } }
Начнем с функции чтения даты из JSON. Для парсинга даты/времени в C# имеется замечательный метод ParseExact()
, позволяющий задать необходимый нам формат, используемый в строке для задания даты/времени. Им мы и воспользуемся:
public class JsonDateTimeConverter : JsonConverter<DateTime> { private readonly string _format = "yyyy-MM-ddThh:mm:sszzz"; public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.ParseExact(reader.GetString(), _format, null); } }
здесь мы задали поле _format
в котором будем хранить формат даты, далее, используя метод объекта reader.GetString()
мы получаем строку со значением даты и, используя метод DateTime.ParseExact()
с заданным форматом, преобразуем эту строку в значение DateTime
.
Метод записи значения DateTime
в JSON будет ещё проще:
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(_format)); }
здесь мы, используя метод writer.WriteStringValue()
записываем значение даты в JSON. В принципе, если бы все даты имели в JSON-объекте один и тот же формат, то на этом, наш конвертер был бы готов к работе и его можно было бы использовать следующим образом:
string json = @"{""registration_date"": ""2002-07-25T20:00:00.000000Z"", ""updated_at"": ""2023-05-10T08:26:21+0300""}"; JsonSerializerOptions options = new(); options.Converters.Add(new JsonDateTimeConverter("yyyy-MM-ddTHH:mm:sszzz")); var dt = JsonSerializer.Deserialize<Dt>(json, options);
в данном случае, мы добавляем наш конвертер в список конвертеров объекта JsonSerializerOptions
и используем его для преобразований для всех дат в объекте. Но, если мы сейчас попробуем воспользоваться приведенным выше способом, то получим уже другую ошибку:
Тот формат, который ранее поддерживался, теперь используется в пользовательском конвертере конвертере и конвертер выдает ошибку парсинга. Чтобы избежать такой проблемы и десериализовать два поля даты, имеющих разный формат, мы можем воспользоваться одним из атрибутов для сериализации, а именно JsonConverter
.
Атрибут JsonConverter
Атрибут JsonConverter
может применяться как к отдельному свойству так и к классу целиком. Этот атрибут принимает в качестве параметра тип конвертера. При этом, сам конвертер должен иметь открытый конструктор без параметров. Таким образом, наш пользовательский конвертер необходимо дописать следующим образом:
public class JsonDateTimeConverter : JsonConverter<DateTime> { private readonly string _format = "yyyy-MM-ddThh:mm:sszzz"; public JsonDateTimeConverter(string format) { _format = format; } public JsonDateTimeConverter() { } public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.ParseExact(reader.GetString(), _format, null); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(_format)); } }
Теперь можно применить атрибут JsonConverter
к свойству класса и проверить работу по десериализации:
public class Dt { [JsonPropertyName("registration_date")] public DateTime RegDate { get; set; } [JsonPropertyName("updated_at")] [JsonConverter(typeof(JsonDateTimeConverter))] public DateTime Updated { get; set; } }
Таким образом, при десериализации JSON, к свойству RegDate
будет применяться стандартный конвертер, а к свойству Updated
— пользовательский JsonDateTimeConverter
. Проверим работу:
public class JsonDateTimeConverter : JsonConverter<DateTime> { private readonly string _format = "yyyy-MM-ddThh:mm:sszzz"; public JsonDateTimeConverter(string format) { _format = format; } public JsonDateTimeConverter() { } public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.ParseExact(reader.GetString(), _format, null); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(_format)); } } public class Dt { [JsonPropertyName("registration_date")] public DateTime RegDate { get; set; } [JsonPropertyName("updated_at")] [JsonConverter(typeof(JsonDateTimeConverter))] public DateTime Updated { get; set; } } internal class Program { string json = @"{""registration_date"": ""2002-07-25T20:00:00.000000Z"", ""updated_at"": ""2023-05-10T08:26:21+0300""}"; var dt = JsonSerializer.Deserialize<Dt>(json); Console.WriteLine(dt.RegDate); Console.WriteLine(dt.Updated); } }
Результат работы в консоли:
10.05.2023 11:26:21
Итого
Пользовательские конвертеры JSON позволяют настроить свою логику сериализации или десериализации JSON. Для создания пользовательского конвертера необходимо унаследовать класс конвертера от класса JsonConverter<T>
и переопределить методы Read()
и Write()
. Для использования пользовательского конвертера JSON мы должны добавить его в список Converters при определении настроек сериализатора или использовать атрибут JsonConverter
в параметр которого необходимо передать тип конвертера, который имеет конструктор без параметров.