Пользовательские конвертеры. Класс JsonConvert

В подавляющем большинстве случаев, сериализатор из 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);
        }
    }
}
System.Text.Json.JsonException: «The JSON value could not be converted to System.DateTime. Path: $.updated_at | LineNumber: 1 | BytePositionInLine: 69.»

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

System.FormatException: «String ‘2002-07-25T20:00:00.000000Z’ was not recognized as a valid DateTime.»

Тот формат, который ранее поддерживался, теперь используется в пользовательском конвертере конвертере и конвертер выдает ошибку парсинга. Чтобы избежать такой проблемы и десериализовать два поля даты, имеющих разный формат, мы можем воспользоваться одним из атрибутов для сериализации, а именно 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);
    }
}

Результат работы в консоли:

25.07.2002 20:00:00
10.05.2023 11:26:21

Итого

Пользовательские конвертеры JSON позволяют настроить свою логику сериализации или десериализации JSON. Для создания пользовательского конвертера необходимо унаследовать класс конвертера от класса JsonConverter<T> и переопределить методы Read() и Write(). Для использования пользовательского конвертера JSON мы должны добавить его в список Converters при определении настроек сериализатора или использовать атрибут JsonConverter в параметр которого необходимо передать тип конвертера, который имеет конструктор без параметров.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии