1. Введение
Сериализация простых объектов — это как отправлять по почте открытку: всё просто, ничего не потеряешь. Но часто наши объекты становятся "семейными альбомами": в них есть вложенные объекты, коллекции и даже коллекции коллекций.
Представьте, что наш пользователь — не просто Person с именем и возрастом, а, например, у него есть набор контактов (Contact), несколько домашних/рабочих адресов, а может быть и поле типа List<Pet>, если человек любит животных. Всё это — вложенные объекты и коллекции.
Как сериализаторы .NET с этим справляются? Какие нюансы надо учитывать, когда мы сериализуем сложные деревья объектов? Может ли всё сломаться, если в коллекциях есть другие коллекции? Сегодня мы разберёмся, чего ожидать — и что делать в сложных случаях.
Что значит "вложенность" в терминах .NET
Вложенный объект — это просто ещё один объект как свойство или поле внутри вашего основного объекта. Например, вот наша расширенная модель пользователя:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<Contact> Contacts { get; set; } // Коллекция вложенных объектов
public Address? HomeAddress { get; set; } // Один вложенный объект (nullable)
}
public class Contact
{
public string Type { get; set; } // Например, Email или Phone
public string Value { get; set; }
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
Обратите внимание — у человека может быть несколько контактов, а адрес — только один (и он может быть null). Это классика многих моделей.
2. Сериализация объектов и коллекций с System.Text.Json
Простой пример: сериализуем и десериализуем коллекцию
Давайте посмотрим, как это работает в жизни:
using System.Text.Json;
var person = new Person
{
Name = "Анна",
Age = 28,
Contacts = new List<Contact>
{
new Contact { Type = "Email", Value = "anna@example.com" },
new Contact { Type = "Phone", Value = "+1234567890" }
},
HomeAddress = new Address { City = "Берлин", Street = "Александрплатц, 1" }
};
string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
Результат будет примерно такой:
{
"Name": "Анна",
"Age": 28,
"Contacts": [
{
"Type": "Email",
"Value": "anna@example.com"
},
{
"Type": "Phone",
"Value": "+1234567890"
}
],
"HomeAddress": {
"City": "Берлин",
"Street": "Александрплатц, 1"
}
}
Сериализатор прекрасно понимает вложенность. Если свойство — это другой объект, он сериализует его "вложенно". Если список — сериализует как массив.
Факт: Такая сериализация автоматически работает для любых коллекций и вложенных объектов, если они публичны и имеют публичный геттер и сеттер (get/set).
Десериализация: работает "из коробки"
string jsonInput = /* JSON, который мы только что получили */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Анна"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"
Всё работает — контакты превращаются из массива JSON обратно в List<Contact>, HomeAddress — обратно в объект класса Address.
3. Как сериализуются коллекции: List, массивы, Dictionary
Часто внутри вашей модели есть не только списки (List<T>), но и словари (Dictionary<TKey, TValue>) или многомерные массивы.
Пример с массивом
public class Team
{
public string Name { get; set; }
public Person[] Members { get; set; }
}
var team = new Team
{
Name = "Разработчики",
Members = new[]
{
new Person { Name = "Алексей", Age = 31 },
new Person { Name = "Екатерина", Age = 27 }
}
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Name": "Разработчики",
"Members": [
{
"Name": "Алексей",
"Age": 31,
"Contacts": null,
"HomeAddress": null
},
{
"Name": "Екатерина",
"Age": 27,
"Contacts": null,
"HomeAddress": null
}
]
}
Сериализация коллекции похожа на упаковку кучи одинаковых корреспонденций: каждый элемент — отдельная "открытка".
Пример с Dictionary
public class Phonebook
{
public Dictionary<string, string> Phones { get; set; }
}
var phonebook = new Phonebook
{
Phones = new Dictionary<string, string>
{
{ "Андрей", "+12998887766" },
{ "Мария", "+12882223344" }
}
};
string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Phones": {
"Андрей": "+12998887766",
"Мария": "+12882223344"
}
}
В JSON словарь становится объектом с динамическими ключами.
4. Вложенные коллекции (List внутри List, массивы массивов)
.NET умеет сериализовывать и такие "матрёшки":
public class Zoo
{
public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }
var zoo = new Zoo
{
AnimalGroups = new List<List<Animal>>
{
new List<Animal> { new Animal { Name = "Лев" }, new Animal { Name = "Тигр" } },
new List<Animal> { new Animal { Name = "Медведь" } }
}
};
string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"AnimalGroups": [
[ { "Name": "Лев" }, { "Name": "Тигр" } ],
[ { "Name": "Медведь" } ]
]
}
То же — обратно на этапе десериализации.
5. Особенности сериализации вложенных объектов в XmlSerializer
Пример сериализации вложенного объекта
using System.Xml.Serialization;
using System.IO;
var person = new Person
{
Name = "Иван",
Age = 35,
HomeAddress = new Address { City = "Бонн", Street = "Бетховен штр., 100" },
Contacts = new List<Contact>
{
new Contact { Type = "Email", Value = "ivan@domain.de" }
}
};
var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());
Результат:
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Иван</Name>
<Age>35</Age>
<Contacts>
<Contact>
<Type>Email</Type>
<Value>ivan@domain.de</Value>
</Contact>
</Contacts>
<HomeAddress>
<City>Бонн</City>
<Street>Бетховен штр., 100</Street>
</HomeAddress>
</Person>
Особенности коллекций в XmlSerializer
По умолчанию XmlSerializer сериализует коллекцию как тег "Contacts", внутри которого для каждого элемента создаётся отдельный тег "Contact". Для этого тип коллекции должен быть публичным, а её элементы — сериализуемыми типами.
Важный момент. Если коллекция пустая, XML всё равно включает пустой тег:
<Contacts />
Поддерживаемые коллекции. XmlSerializer поддерживает List<T>, массивы T[], коллекции, реализующие ICollection<T>. Не поддерживает, например, Dictionary<TKey, TValue>! Для сериализации словарей придётся использовать дополнительные техники или сторонние библиотеки.
6. Поддержка объектов и коллекций в Newtonsoft.Json
Newtonsoft.Json поддерживает вложенность чуть более гибко. В 99% случаев всё работает так же, как и в System.Text.Json, но есть плюсы: можно сериализовать даже приватные поля (если явно указать), словари с более сложными ключами, динамические типы, и даже циклические ссылки (с помощью специальных настроек).
Пример
using Newtonsoft.Json;
var person = new Person
{
Name = "Павел",
Age = 40,
Contacts = new List<Contact>
{
new Contact { Type = "SMS", Value = "+10000000013" }
}
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);
Отличие. Если у объекта, например, имеются приватные поля, их можно сериализовать настройками ContractResolver. Но для базовых вложенных коллекций и объектов всё автоматически работает "из коробки".
7. Типичные ошибки, подводные камни и нюансы
Ошибка 1: Вложенные объекты — null.
Если у объекта, который вы сериализуете, некоторые свойства не заполнены (null), то в JSON или XML их может не быть вовсе, либо они будут записаны как null. Например, если HomeAddress у Person равен null, то в JSON:
"HomeAddress": null
В XML по умолчанию такого тега не будет — его можно добавить настройками сериализатора ([XmlElement(IsNullable=true)]).
Ошибка 2: Сериализация приватных свойств/полей.
По умолчанию большинство сериализаторов (и JSON, и XML) работают только с публичными свойствами. Если вы хотите сериализовать приватные или защищённые поля, придётся объявить их публичными или настраивать сериализатор (например, через ContractResolver в Newtonsoft.Json).
Ошибка 3: Классы без конструктора по умолчанию.
В XML-сериализации (да и часто в JSON) класс должен иметь публичный конструктор без параметров. Если такого конструктора нет — будет исключение при десериализации.
Ошибка 4: Сериализация словарей в XML.
XmlSerializer не поддерживает сразу сериализацию Dictionary<TKey, TValue>. Это часто становится неожиданностью. Решением может быть заворачивание словаря в список пар или использование других сериализаторов.
Ошибка 5: Циклические ссылки.
Если один объект через ссылку содержит другой объект, который в свою очередь снова ссылается на первый (например, родитель и ребёнок), сериализация "по кругу" может привести к ошибке переполнения стека (StackOverflowException) или к исключению о циклической ссылке. В JSON-сериализаторах это можно обойти настройками, но часто правильнее пересмотреть архитектуру объекта.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ