JavaRush /Курсы /C# SELF /Глубокое погружение в Syst...

Глубокое погружение в System.Text.Json

C# SELF
47 уровень , 1 лекция
Открыта

1. Введение

На этом уроке мы сделаем шаг от "дай мне просто сохранить список" к гибкой, точной и производительной сериализации с помощью System.Text.Json. В современных .NET‑проектах JSON — де‑факто стандарт обмена данными. Базовые вызовы Serialize/Deserialize просты, но реальные задачи требуют тонкой настройки: игнорирование/переименование полей, управление форматами дат, защита от циклов, работа с большими объёмами данных, собственные конвертеры и т.д.

Мы рассмотрим не только методы JsonSerializer, но и параметры через JsonSerializerOptions, атрибуты, работу с Stream, управление памятью и внедрение своих правил сериализации через JsonConverter.

Краткая история и позиционирование System.Text.Json

Долгое время в .NET доминировал Newtonsoft.Json (Json.NET) — гибкий и зрелый, но не всегда самый быстрый и лёгкий по зависимостям. С .NET Core 3.0 появился встроенный System.Text.Json: высокая производительность, минимум зависимостей (в составе платформы), плотная интеграция с ASP.NET Core и постоянное развитие с релизами .NET.

2. Основные классы и методы

Базовый элемент — статический класс JsonSerializer, который даёт два направления:

  • Сериализация: объект → JSON‑строка (Serialize)
  • Десериализация: JSON‑строка → объект нужного типа (Deserialize)

Пример сериализации простого объекта

using System.Text.Json;

var person = new Person { Name = "Иван", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"Иван","Age":30}

Пример десериализации

var json = "{\"Name\":\"Анна\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // Анна

Примечание: тип Person уже реализован в предыдущих лекциях — используем его и здесь.

3. Управление сериализацией: JsonSerializerOptions

В реальных проектах почти всегда нужны настройки: имена свойств в camelCase, форматы дат, обработка циклов, значения по умолчанию и т.д. Всё это регулируется через JsonSerializerOptions.

Пример настройки

var options = new JsonSerializerOptions
{
    WriteIndented = true,    // Красиво форматировать JSON (добавляет пробелы и переносы)
    PropertyNameCaseInsensitive = true, // Игнорировать регистр имён свойств при десериализации
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase для свойств (а не PascalCase)
};

string json = JsonSerializer.Serialize(person, options);
/*
{
  "name": "Иван",
  "age": 30
}
*/

Почему это важно? Большинство фронтенд‑фреймворков ожидают именно camelCase, а не .NET‑шный PascalCase.

4. Атрибуты: System.Text.Json.Serialization

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

Основные атрибуты

Атрибут Что делает
[JsonIgnore]
Исключает свойство из сериализации/десериализации
[JsonPropertyName("имя")]
Использует другое имя в JSON
[JsonInclude]
Включает не‑публичное свойство/поле в сериализацию
[JsonNumberHandling]
Управляет обработкой числовых значений

Пример: управление свойствами через атрибуты

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("full_name")]
    public string Name { get; set; }

    [JsonIgnore]
    public int SecretCode { get; set; }

    public int Age { get; set; }
}
var person = new Person { Name = "Пётр", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"Пётр","Age":45}

Обратите внимание: SecretCode не попал в JSON, а Name сериализовался как "full_name".

5. Сериализация коллекций и вложенных объектов

Коллекции — это просто

var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]

var people = new List<Person> {
    new Person { Name = "Анна", Age = 20 },
    new Person { Name = "Максим", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"Анна","Age":20},{"Name":"Максим","Age":40}]

Вложенные структуры

public class Group
{
    public string Name { get; set; }
    public List<Person> Members { get; set; }
}

var group = new Group
{
    Name = "Разработчики",
    Members = new List<Person>
    {
        new Person { Name = "Саша", Age = 23 },
        new Person { Name = "Маша", Age = 28 }
    }
};

string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
  "name": "Разработчики",
  "members": [
    { "name": "Саша", "age": 23 },
    { "name": "Маша", "age": 28 }
  ]
}
*/

6. Десериализация: что важно знать?

var json = "[{\"Name\":\"Иван\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // Иван

Частый сценарий: если в JSON нет какого‑то поля, соответствующее свойство получит значение по умолчанию. Лишние поля в JSON, которых нет в модели, игнорируются. Но если типы не совпадают (например, вместо числа пришла строка) — при десериализации будет выброшено исключение.

7. Обработка дат, времени, форматов и числовых значений

public class Meeting
{
    public string Topic { get; set; }
    public DateTime Time { get; set; }
}

var meeting = new Meeting { Topic = "Собрание", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"Собрание","Time":"2024-06-06T20:30:00.0000000+03:00"}

По умолчанию DateTime сериализуется в ISO 8601. Нужен другой вид (например, только дата)? Используйте отдельное свойство или кастомный конвертер (см. ниже).

FAQ: Чтобы числа сериализовались как строки (например, телефоны или большие ID), используйте атрибут [JsonNumberHandling(JsonNumberHandling.WriteAsString)].

8. Потоки и работа с файлами

Работать можно не только со строками, но и с Stream — это важно для больших данных (файлы, сеть).

Пример записи в файл

using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// Не забудьте вызвать fs.Flush() или использовать using!

Пример чтения из файла

using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);

С потоками доступны асинхронные методы SerializeAsync/DeserializeAsync — пригодится для высоконагруженных сервисов.

9. Кастомные конвертеры

Если стандартные правила не подходят (нестандартные форматы дат/чисел, сложные значения, собственные структуры) — пишем JsonConverter.

Пример: дата только как "dd.MM.yyyy"

public class CustomDateConverter : JsonConverter<DateTime>
{
    public override DateTime Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
    }

    public override void Write(
        Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
    }
}

var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());

var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"

Кастомные конвертеры полезны для сериализации координат, векторов, цветов, нестандартных дат и валют, различных ID‑форматов и др.

10. Полезные нюансы

Обработка циклических ссылок и глубоких иерархий

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve, // Сохраняет все объекты с $id/$ref
    WriteIndented = true
};

Важно: в JSON появятся служебные свойства $id и $ref. Для обмена с внешними системами, которые их не понимают, это может не подойти.

Различия между System.Text.Json и Newtonsoft.Json

System.Text.Json уже очень мощен, но пока не закрывает абсолютно все сценарии Newtonsoft.Json (приватные конструкторы, сложные динамические объекты и т.п.). Для большинства стандартных задач рекомендуем встроенный сериализатор — он быстрее и без лишних зависимостей.

Интерактивная работа с JSON: DOM API

Когда нужно «пройтись» по JSON без полной модели, используйте JsonDocument и JsonElement.

using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;

if (root.TryGetProperty("Name", out var nameProperty))
{
    Console.WriteLine(nameProperty.GetString());
}

11. Полезные опции и их эффекты

Свойство Значение/Назначение
WriteIndented
true — форматирование с отступами
PropertyNameCaseInsensitive
true — игнорировать регистр имён свойств при десериализации
PropertyNamingPolicy
JsonNamingPolicy.CamelCase
DefaultIgnoreCondition
Правила игнорирования null/значений по умолчанию
ReferenceHandler
Preserve
,
IgnoreCycles
AllowTrailingCommas
true — разрешить запятую в конце массива
NumberHandling
Преобразование чисел в строки/обратно (и др.)
Converters
Список кастомных конвертеров

12. Частые ошибки и практические советы

Ошибка №1: неправильный тип при десериализации. Если сериализовали список, десериализуйте именно в список: List<T>, а не одиночный объект.

Ошибка №2: неверный регистр имён свойств. Без настройки регистрозависимости свойства могут «не находиться». Используйте PropertyNameCaseInsensitive или настройте PropertyNamingPolicy.

Ошибка №3: некорректная обработка дат. По умолчанию формат — ISO 8601. Нужен другой — создайте и примените конвертер (JsonConverter<DateTime>).

Ошибка №4: ожидание сериализации приватных/статических полей. По умолчанию берутся публичные свойства. Для нестандартных случаев используйте соответствующие атрибуты (например, [JsonInclude]).

Ошибка №5: непонимание значений по умолчанию. Отсутствующее поле в JSON → значение по умолчанию для свойства. Учитывайте это в логике.

Ошибка №6: неправильное управление потоками. Закрывайте ресурсы с using или await using, чтобы избежать утечек и блокировок.

2
Задача
C# SELF, 47 уровень, 1 лекция
Недоступна
Сериализация объекта с настройками JsonSerializerOptions
Сериализация объекта с настройками JsonSerializerOptions
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ