JavaRush /Курсы /C# SELF /Работа с вложенными объектами и коллекциями

Работа с вложенными объектами и коллекциями

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

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-сериализаторах это можно обойти настройками, но часто правильнее пересмотреть архитектуру объекта.

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