1. Введение
Как бы ни хотелось думать, что коллекции сериализуются и десериализуются идеально “из коробки”, в реальных проектах это часто не так. Иногда нужно скрыть определённые коллекции от сериализации — например, внутренние кэшированные данные. Бывает, требуется переименовать коллекционные свойства, чтобы они соответствовали контракту API. В некоторых случаях важно управлять тем, какие элементы сохраняются или игнорируются, или даже преобразовать коллекцию специальным образом, чтобы итоговый JSON выглядел понятным и “читаемым” для других сервисов.
К счастью, System.Text.Json предлагает простой и прозрачный способ управления сериализацией с помощью атрибутов, которые можно применять как к коллекциям, так и к отдельным элементам. В этом разделе мы продолжим развивать нашу библиотечную модель, чтобы разобраться, как это работает на практике.
2. Исключение коллекционных свойств: [JsonIgnore]
Начнем с простого. Иногда в вашем классе есть коллекция, которую сериализовать нельзя — например, это временные, кэшированные или чувствительные данные. Что делать? Конечно же, [JsonIgnore]!
Представим, что у нас есть класс Library, в который мы добавили свойство List<Book> Cache, используемое только для быстрого доступа:
using System.Text.Json.Serialization;
public class Library
{
public string Name { get; set; }
public List<Book> Books { get; set; }
[JsonIgnore]
public List<Book> Cache { get; set; } // Не сериализуется!
}
// Пример использования:
var library = new Library
{
Name = "Главная библиотека",
Books = new List<Book>
{
new Book { Title = "Волшебная долина", Author = new Author { Name = "Туве Янссен", BirthYear = 1914 } }
},
Cache = new List<Book>
{
new Book { Title = "Повелитель мух", Author = new Author { Name = "Вильям Голдинг", BirthYear = 1911 } }
}
};
string json = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json); // В JSON-е нет свойства Cache!
Результат сериализации будет примерно такой:
{
"Name": "Главная библиотека",
"Books": [
{
"Title": "Волшебная долина",
"Author": {
"Name": "Туве Янссен",
"BirthYear": 1914
}
}
]
}
Видите? Никаких “кэшей” во внешнем мире. Всё, что под [JsonIgnore], — скрыто и надёжно, как Wi-Fi пароль в вашей голове.
3. Переименование коллекций с [JsonPropertyName]
Часто сталкиваетесь с такими API, где ожидают, например, "items" вместо "Books"? Или, наоборот, вы не хотите переименовывать поле в C# (чтобы не запутаться), но в JSON оно должно “звучать” иначе?
Вот как это делается:
using System.Text.Json.Serialization;
public class Library
{
public string Name { get; set; }
[JsonPropertyName("items")]
public List<Book> Books { get; set; }
[JsonIgnore]
public List<Book> Cache { get; set; }
}
// Сериализация:
var library = new Library { Name = "Филиал №1", Books = new List<Book>() };
string json = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
Вывод:
{
"Name": "Филиал №1",
"items": []
}
Обратите внимание, что десериализация тоже корректно сопоставит поле items из JSON с Books в C# — магия работает в обе стороны.
4. Управление сериализацией коллекций и их элементов
Для этого вам понадобится JsonIgnoreCondition.WhenWritingNull и/или nullable.
Бывает, что коллекция — это просто необязательное поле. Например, у только что созданной библиотеки пока ещё нет книг. Если вы не хотите, чтобы в JSON появлялось свойство books: null, вы можете управлять этим через опции:
var library = new Library { Name = "Пустая библиотека" };
// Books не инициализирован = null
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
string json = JsonSerializer.Serialize(library, options);
Console.WriteLine(json);
Результат:
{
"Name": "Пустая библиотека"
}
А если у вас есть пустой список (но не null), то сериализатор выдаст "books": []. Это важное различие, потому что иногда нужно намеренно скрыть поле, если оно null, но не пустой список.
5. Атрибут [JsonIgnore] на свойствах элементов
Атрибуты сериализации работают и внутри элементов коллекции. Вы можете скрыть отдельные свойства у каждого объекта в списке.
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
[JsonIgnore]
public string InternalCode { get; set; }
}
Теперь при сериализации книги из коллекции Books, поля InternalCode не будет в JSON.
6. Адресация коллекций через “индексы” или вложенные структуры
Иногда приходится сериализовать коллекции не просто как массивы, а, например, как “карты” (dictionary) — если каждая книга имеет уникальный идентификатор. В этом случае — без хитрых атрибутов для элементов, но с помощью стандартных средств — можно объявить свойство-словарь:
public class Library
{
[JsonPropertyName("catalog")]
public Dictionary<string, Book> BookCatalog { get; set; }
}
При сериализации словарь превратится в объект с парами ключ-значение:
var library = new Library
{
BookCatalog = new Dictionary<string, Book>
{
["978-5-699-12345-6"] = new Book { Title = "Словарь", Author = new Author { Name = "Неизвестный", BirthYear = 2000 } }
}
};
JSON:
{
"catalog": {
"978-5-699-12345-6": {
"Title": "Словарь",
"Author": {
"Name": "Неизвестный",
"BirthYear": 2000
}
}
}
}
Такое представление удобно для передачи по API, где важно сохранять связь между ключом и объектом.
7. Ошибки и трудности при управлении сериализацией коллекций
Если попытаться сериализовать коллекцию, у которой не все элементы корректно инициализированы (например, в списке есть null), то по умолчанию System.Text.Json запишет такие элементы как null в массиве.
Даже если установлено DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, элементы-null внутри массива останутся — правило относится к свойствам объекта, а не к содержимому коллекций. Чтобы этого избежать, предварительно очистите коллекцию: RemoveAll(b => b == null).
Частая путаница при десериализации — несоответствие имён. Если забыть указать [JsonPropertyName], класс будет ожидать свойство Books, а вы отправите JSON с items: в результате коллекция не заполнится и останется пустой. Всегда проверяйте корректность имён!
8. Таблица: где применимы основные атрибуты
| Атрибут | Можно ли применять к коллекциям? | Можно ли применять к элементам коллекций? | Примеры использования |
|---|---|---|---|
|
да | да | Скрыть список или поле внутри Book |
|
да | да | Переименовать Books → items или Title → name |
|
да | да | Включить приватные свойства в сериализацию |
|
да | да | Назначить специальный конвертер для списка |
9. Схема сериализации коллекций с атрибутами
+-------------+
| Library |
+-------------+
| Name -- сериализуется как "Name"
| Books -- [JsonPropertyName("items")], сериализуется как "items": [...]
| Cache -- [JsonIgnore], не сериализуется
| BookCatalog -- [JsonPropertyName("catalog")], сериализуется как "catalog": {...}
JSON-результат примерно такой:
{
"Name": "Городская библиотека",
"items": [
{
"Title": "1984",
"Author": {
"Name": "Джордж Оруэлл",
"BirthYear": 1903
}
},
{
"Title": "Большие надежды",
"Author": {
"Name": "Чарльз Диккенс",
"BirthYear": 1812
}
}
],
"catalog": {
"978-1234567890": {
"Title": "Зов Ктулху",
"Author": {
"Name": "Говард Филлипс Лавкрафт",
"BirthYear": 1890
}
}
}
}
10. Практическое значение и особенности на собеседованиях и в реальных проектах
В “боевых” условиях всегда приходится учитывать контракт внешнего API и требования к сериализации. Нужно уметь “прятать” внутренние коллекции, соответствовать регистру и стилю имён, иногда даже динамически менять схему сериализации в зависимости от версии клиента.
Типичные вопросы на собеседованиях:
- Как сериализовать только часть данных?
- Как сделать, чтобы свойство коллекции не попало в JSON?
- Как сопоставить имена свойств в C# и JSON, если они различаются?
- Можно ли скрыть отдельные элементы внутри коллекции от сериализации (например, конфиденциальную информацию)?
Ответы крутятся вокруг грамотного использования атрибутов и параметров сериализации: [JsonIgnore], [JsonPropertyName], опций JsonSerializerOptions и продуманной работы с содержимым коллекций.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ