1. Wprowadzenie
Serializacja prostych obiektów — to jak wysyłanie pocztówki: wszystko proste, nic nie zginie. Ale często nasze obiekty stają się "rodzinnym albumem": mają zagnieżdżone obiekty, kolekcje, a nawet kolekcje kolekcji.
Wyobraź sobie, że nasz użytkownik to nie tylko Person z imieniem i wiekiem, ale np. ma zestaw kontaktów (Contact), kilka adresów domowych/służbowych, a może pole typu List<Pet>, jeśli osoba lubi zwierzęta. To wszystko to zagnieżdżone obiekty i kolekcje.
Jak serializatory .NET sobie z tym radzą? Jakie niuanse trzeba wziąć pod uwagę, kiedy serializujemy złożone drzewa obiektów? Czy wszystko może się zepsuć, jeśli w kolekcjach są inne kolekcje? Dziś się tego dowiemy — czego się spodziewać i co robić w trudnych przypadkach.
Co znaczy "zagnieżdżenie" w terminach .NET
Zagnieżdżony obiekt to po prostu kolejny obiekt jako właściwość lub pole wewnątrz twojego głównego obiektu. Na przykład, oto nasz rozszerzony model użytkownika:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<Contact> Contacts { get; set; } // Kolekcja zagnieżdżonych obiektów
public Address? HomeAddress { get; set; } // Jeden zagnieżdżony obiekt (nullable)
}
public class Contact
{
public string Type { get; set; } // Na przykład Email lub Phone
public string Value { get; set; }
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
Zwróć uwagę — osoba może mieć kilka kontaktów, a adres tylko jeden (i może być null). To klasyka wielu modeli.
2. Serializacja obiektów i kolekcji z System.Text.Json
Prosty przykład: serializujemy i deserializujemy kolekcję
Sprawdźmy, jak to działa w praktyce:
using System.Text.Json;
var person = new Person
{
Name = "Anna",
Age = 28,
Contacts = new List<Contact>
{
new Contact { Type = "Email", Value = "anna@example.com" },
new Contact { Type = "Phone", Value = "+1234567890" }
},
HomeAddress = new Address { City = "Berlin", Street = "Alexanderplatz, 1" }
};
string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
Wynik będzie mniej więcej taki:
{
"Name": "Anna",
"Age": 28,
"Contacts": [
{
"Type": "Email",
"Value": "anna@example.com"
},
{
"Type": "Phone",
"Value": "+1234567890"
}
],
"HomeAddress": {
"City": "Berlin",
"Street": "Alexanderplatz, 1"
}
}
Serializator świetnie rozumie zagnieżdżenie. Jeśli właściwość to inny obiekt, serializuje go "zagnieżdżenie". Jeśli lista — serializuje jako tablicę.
Fakt: taka serializacja działa automatycznie dla dowolnych kolekcji i zagnieżdżonych obiektów, jeśli są publiczne i mają publiczny getter i setter (get/set).
Deserializacja: działa "out of the box"
string jsonInput = /* JSON, któryśmy właśnie otrzymali */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"
Wszystko działa — kontakty zamieniają się z tablicy JSON z powrotem w List<Contact>, HomeAddress — z powrotem w obiekt klasy Address.
3. Jak serializowane są kolekcje: List, tablice, Dictionary
Często w twoim modelu są nie tylko listy (List<T>), ale też słowniki (Dictionary<TKey, TValue>) lub wielowymiarowe tablice.
Przykład z tablicą
public class Team
{
public string Name { get; set; }
public Person[] Members { get; set; }
}
var team = new Team
{
Name = "Programiści",
Members = new[]
{
new Person { Name = "Alex", Age = 31 },
new Person { Name = "Kate", Age = 27 }
}
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Name": "Programiści",
"Members": [
{
"Name": "Alex",
"Age": 31,
"Contacts": null,
"HomeAddress": null
},
{
"Name": "Kate",
"Age": 27,
"Contacts": null,
"HomeAddress": null
}
]
}
Serializacja kolekcji jest podobna do pakowania sterty identycznych pocztówek: każdy element to osobna "kartka".
Przykład z Dictionary
public class Phonebook
{
public Dictionary<string, string> Phones { get; set; }
}
var phonebook = new Phonebook
{
Phones = new Dictionary<string, string>
{
{ "Andy", "+12998887766" },
{ "Maria", "+12882223344" }
}
};
string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Phones": {
"Andy": "+12998887766",
"Maria": "+12882223344"
}
}
W JSON słownik staje się obiektem z dynamicznymi kluczami.
4. Zagnieżdżone kolekcje (List wewnątrz List, tablice tablic)
.NET potrafi serializować też takie "matrioszki":
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 = "Lew" }, new Animal { Name = "Tygrys" } },
new List<Animal> { new Animal { Name = "Niedźwiedź" } }
}
};
string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"AnimalGroups": [
[ { "Name": "Lew" }, { "Name": "Tygrys" } ],
[ { "Name": "Niedźwiedź" } ]
]
}
To samo — przy deserializacji działa w drugą stronę.
5. Cechy serializacji zagnieżdżonych obiektów w XmlSerializer
Przykład serializacji zagnieżdżonego obiektu
using System.Xml.Serialization;
using System.IO;
var person = new Person
{
Name = "Iwan",
Age = 35,
HomeAddress = new Address { City = "Bonn", Street = "Beethoven str., 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());
Wynik:
<?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>Iwan</Name>
<Age>35</Age>
<Contacts>
<Contact>
<Type>Email</Type>
<Value>ivan@domain.de</Value>
</Contact>
</Contacts>
<HomeAddress>
<City>Bonn</City>
<Street>Beethoven str., 100</Street>
</HomeAddress>
</Person>
Cechy kolekcji w XmlSerializer
Domyślnie XmlSerializer serializuje kolekcję jako tag "Contacts", wewnątrz którego dla każdego elementu tworzy osobny tag "Contact". Typ kolekcji musi być publiczny, a jej elementy — typami serializowalnymi.
Ważna rzecz. Jeśli kolekcja jest pusta, XML i tak zawiera pusty tag:
<Contacts />
Obsługiwane kolekcje. XmlSerializer wspiera List<T>, tablice T[], kolekcje implementujące ICollection<T>. Nie wspiera na przykład Dictionary<TKey, TValue>! Do serializacji słowników trzeba użyć opakowania słownika w listę par albo zastosować inne serializatory.
6. Wsparcie obiektów i kolekcji w Newtonsoft.Json
Newtonsoft.Json obsługuje zagnieżdżenia trochę bardziej elastycznie. W 99% przypadków działa podobnie jak System.Text.Json, ale są plusy: można serializować nawet prywatne pola (jeśli jawnie to ustawisz), słowniki z bardziej skomplikowanymi kluczami, typy dynamiczne, a nawet cykliczne referencje (przy użyciu specjalnych ustawień).
Przykład
using Newtonsoft.Json;
var person = new Person
{
Name = "Pawel",
Age = 40,
Contacts = new List<Contact>
{
new Contact { Type = "SMS", Value = "+10000000013" }
}
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);
Różnica. Jeśli obiekt ma prywatne pola, można je serializować przez ustawienia ContractResolver. Ale dla podstawowych zagnieżdżonych kolekcji i obiektów wszystko działa domyślnie "out of the box".
7. Typowe błędy, pułapki i niuanse
Błąd 1: Zagnieżdżone obiekty — null.
Jeśli obiekt, który serializujesz, ma właściwości nie wypełnione (null), to w JSON lub XML mogą one nie wystąpić wcale, albo będą zapisane jako null. Na przykład, jeśli HomeAddress u Person jest null, to w JSON:
"HomeAddress": null
W XML domyślnie takiego taga nie będzie — możesz go dodać ustawieniami serializatora ([XmlElement(IsNullable=true)]).
Błąd 2: Serializacja prywatnych właściwości/pól.
Domyślnie większość serializatorów (i JSON, i XML) działa tylko z publicznymi właściwościami. Jeśli chcesz serializować prywatne lub chronione pola, trzeba je uczynić publicznymi lub skonfigurować serializator (np. przez ContractResolver w Newtonsoft.Json).
Błąd 3: Klasy bez konstruktora domyślnego.
W serializacji XML (i często w JSON) klasa powinna mieć publiczny konstruktor bez parametrów. Jeśli takiego konstruktora brak — przy deserializacji dostaniesz wyjątek.
Błąd 4: Serializacja słowników w XML.
XmlSerializer nie obsługuje bezpośrednio serializacji Dictionary<TKey, TValue>. To często bywa niespodzianką. Rozwiązaniem może być opakowanie słownika w listę par albo użycie innych serializatorów.
Błąd 5: Cykliczne referencje.
Jeśli jeden obiekt przez referencję zawiera inny obiekt, który z kolei znowu odwołuje się do pierwszego (np. rodzic i dziecko), serializacja "po okręgu" może doprowadzić do przepełnienia stosu (StackOverflowException) lub wyjątku o cyklicznej referencji. W JSON-serializatorach można to obejść ustawieniami, ale często lepiej przemyśleć architekturę obiektu.
GO TO FULL VERSION