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-серіалізаторах це можна обійти налаштуваннями, але часто правильніше переглянути архітектуру об’єкта.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ