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
Иногда удобнее управлять сериализацией прямо из модели с помощью атрибутов. Их добавляют к полям/свойствам, чтобы влиять на имена, включение/исключение и обработку значений.
Основные атрибуты
| Атрибут | Что делает |
|---|---|
|
Исключает свойство из сериализации/десериализации |
|
Использует другое имя в JSON |
|
Включает не‑публичное свойство/поле в сериализацию |
|
Управляет обработкой числовых значений |
Пример: управление свойствами через атрибуты
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. Полезные опции и их эффекты
| Свойство | Значение/Назначение |
|---|---|
|
true — форматирование с отступами |
|
true — игнорировать регистр имён свойств при десериализации |
|
|
|
Правила игнорирования null/значений по умолчанию |
|
, |
|
true — разрешить запятую в конце массива |
|
Преобразование чисел в строки/обратно (и др.) |
|
Список кастомных конвертеров |
12. Частые ошибки и практические советы
Ошибка №1: неправильный тип при десериализации. Если сериализовали список, десериализуйте именно в список: List<T>, а не одиночный объект.
Ошибка №2: неверный регистр имён свойств. Без настройки регистрозависимости свойства могут «не находиться». Используйте PropertyNameCaseInsensitive или настройте PropertyNamingPolicy.
Ошибка №3: некорректная обработка дат. По умолчанию формат — ISO 8601. Нужен другой — создайте и примените конвертер (JsonConverter<DateTime>).
Ошибка №4: ожидание сериализации приватных/статических полей. По умолчанию берутся публичные свойства. Для нестандартных случаев используйте соответствующие атрибуты (например, [JsonInclude]).
Ошибка №5: непонимание значений по умолчанию. Отсутствующее поле в JSON → значение по умолчанию для свойства. Учитывайте это в логике.
Ошибка №6: неправильное управление потоками. Закрывайте ресурсы с using или await using, чтобы избежать утечек и блокировок.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ