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