1. Wprowadzenie
Niezależnie od tego, jak bardzo by się chciało uważać, że kolekcje serializują się i deserializują idealnie "out of the box", w realnych projektach często tak nie jest. Czasem trzeba ukryć konkretne kolekcje przed serializacją — na przykład wewnętrzne, cache'owane dane. Bywa też, że trzeba zmienić nazwę właściwości kolekcji, żeby pasowała do kontraktu API. W niektórych przypadkach ważne jest kontrolowanie, które elementy są zapisywane lub ignorowane, albo nawet przekształcenie kolekcji w specjalny sposób, żeby wynikowy JSON był czytelny dla innych serwisów.
Na szczęście System.Text.Json oferuje prosty i przejrzysty sposób zarządzania serializacją za pomocą atrybutów, które można stosować zarówno do kolekcji, jak i do poszczególnych elementów. W tej sekcji dalej rozwiniemy nasz model biblioteczny, żeby zobaczyć, jak to działa w praktyce.
2. Wykluczanie właściwości kolekcji: [JsonIgnore]
Zacznijmy od prostego przykładu. Czasami w twojej klasie znajduje się kolekcja, której nie należy serializować — np. to dane tymczasowe, cache'owane lub wrażliwe. Co robić? Oczywiście, [JsonIgnore]!
Wyobraźmy sobie klasę Library, do której dodaliśmy właściwość List<Book> Cache, używaną tylko do szybkiego dostępu:
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; } // Nie serializuje się!
}
// Przykład użycia:
var library = new Library
{
Name = "Główna biblioteka",
Books = new List<Book>
{
new Book { Title = "Magiczna dolina", Author = new Author { Name = "Tove Jansson", BirthYear = 1914 } }
},
Cache = new List<Book>
{
new Book { Title = "Władca much", Author = new Author { Name = "William Golding", BirthYear = 1911 } }
}
};
string json = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json); // W JSON nie ma właściwości Cache!
Wynik serializacji będzie mniej więcej taki:
{
"Name": "Główna biblioteka",
"Books": [
{
"Title": "Magiczna dolina",
"Author": {
"Name": "Tove Jansson",
"BirthYear": 1914
}
}
]
}
Widzisz? Żadnych "cache'ów" na zewnątrz. Wszystko oznaczone [JsonIgnore] — ukryte i bezpieczne, jak hasło do Wi‑Fi w twojej głowie.
3. Zmiana nazwy kolekcji za pomocą [JsonPropertyName]
Często trafiasz na API, które oczekuje np. "items" zamiast "Books"? Albo nie chcesz zmieniać nazwy pola w C# (żeby się nie pogubić), ale w JSON ma być inaczej?
Tak to się robi:
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; }
}
// Serializacja:
var library = new Library { Name = "Oddział №1", Books = new List<Book>() };
string json = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
Wynik:
{
"Name": "Oddział №1",
"items": []
}
Zwróć uwagę, że deserializacja też poprawnie zmapuje pole items z JSON na Books w C# — magia działa w obie strony.
4. Kontrola serializacji kolekcji i ich elementów
Do tego przydadzą się JsonIgnoreCondition.WhenWritingNull i/lub typy nullable.
Bywa, że kolekcja to po prostu pole opcjonalne. Na przykład świeżo utworzona biblioteka może jeszcze nie mieć książek. Jeśli nie chcesz, żeby w JSON pojawiło się pole books: null, możesz sterować tym przez opcje:
var library = new Library { Name = "Pusta biblioteka" };
// Books nie zainicjalizowane = null
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
string json = JsonSerializer.Serialize(library, options);
Console.WriteLine(json);
Wynik:
{
"Name": "Pusta biblioteka"
}
A jeśli masz pustą listę (ale nie null), to serializator wypisze "books": []. To ważna różnica, bo czasem chcesz świadomie ukryć pole, gdy jest null, ale nie gdy jest pustą kolekcją.
5. Atrybut [JsonIgnore] na właściwościach elementów
Atrybuty serializacji działają również wewnątrz elementów kolekcji. Możesz ukryć pojedyncze właściwości każdego obiektu w liście.
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
[JsonIgnore]
public string InternalCode { get; set; }
}
Teraz podczas serializacji książki z kolekcji Books pole InternalCode nie pojawi się w JSON.
6. Adresowanie kolekcji przez "indeksy" albo struktury zagnieżdżone
Czasami trzeba serializować kolekcje nie jako tablice, lecz np. jako "mapy" (dictionary) — jeśli każda książka ma unikalny identyfikator. W takim wypadku — bez magicznych atrybutów dla elementów, a przy użyciu standardowych narzędzi — możesz zadeklarować właściwość słownika:
public class Library
{
[JsonPropertyName("catalog")]
public Dictionary<string, Book> BookCatalog { get; set; }
}
Przy serializacji słownik zamieni się w obiekt z parą klucz-wartość:
var library = new Library
{
BookCatalog = new Dictionary<string, Book>
{
["978-5-699-12345-6"] = new Book { Title = "Słownik", Author = new Author { Name = "Nieznany", BirthYear = 2000 } }
}
};
JSON:
{
"catalog": {
"978-5-699-12345-6": {
"Title": "Słownik",
"Author": {
"Name": "Nieznany",
"BirthYear": 2000
}
}
}
}
Takie przedstawienie jest wygodne do przesyłania przez API, gdzie ważne jest zachowanie relacji między kluczem a obiektem.
7. Błędy i trudności przy zarządzaniu serializacją kolekcji
Jeśli spróbujesz serializować kolekcję, w której nie wszystkie elementy są poprawnie zainicjalizowane (np. w liście są null), to domyślnie System.Text.Json zapisze takie elementy jako null w tablicy.
Nawet jeśli ustawisz DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, elementy-null wewnątrz tablicy pozostaną — to ustawienie dotyczy właściwości obiektu, a nie zawartości kolekcji. Aby temu zapobiec, przed serializacją oczyść kolekcję: RemoveAll(b => b == null).
Częsta pomyłka przy deserializacji — niezgodność nazw. Jeśli zapomnisz dodać [JsonPropertyName], klasa będzie oczekiwać właściwości Books, a wy wyślecie JSON z items: w rezultacie kolekcja nie zostanie wypełniona i zostanie pusta. Zawsze sprawdzaj poprawność nazw!
8. Tabela: gdzie stosować podstawowe atrybuty
| Atrybut | Czy można stosować do kolekcji? | Czy można stosować do elementów kolekcji? | Przykłady użycia |
|---|---|---|---|
|
tak | tak | Ukryć listę lub pole w Book |
|
tak | tak | Zamienić Books → items lub Title → name |
|
tak | tak | Dołączyć prywatne właściwości do serializacji |
|
tak | tak | Przypisać specjalny konwerter dla listy |
9. Schemat serializacji kolekcji z atrybutami
+-------------+
| Library |
+-------------+
| Name -- serializuje się jako "Name"
| Books -- [JsonPropertyName("items")], serializuje się jako "items": [...]
| Cache -- [JsonIgnore], nie serializuje się
| BookCatalog -- [JsonPropertyName("catalog")], serializuje się jako "catalog": {...}
Wynik JSON mniej więcej taki:
{
"Name": "Miejska biblioteka",
"items": [
{
"Title": "1984",
"Author": {
"Name": "George Orwell",
"BirthYear": 1903
}
},
{
"Title": "Wielkie nadzieje",
"Author": {
"Name": "Charles Dickens",
"BirthYear": 1812
}
}
],
"catalog": {
"978-1234567890": {
"Title": "Zew Cthulhu",
"Author": {
"Name": "Howard Phillips Lovecraft",
"BirthYear": 1890
}
}
}
}
10. Praktyczne znaczenie i cechy podczas rozmów kwalifikacyjnych i w realnych projektach
W "bojowych" warunkach zawsze trzeba uwzględniać kontrakt zewnętrznego API i wymagania dotyczące serializacji. Trzeba umieć "chować" wewnętrzne kolekcje, dopasować styl i wielkość liter w nazwach, a czasem nawet dynamicznie zmieniać schemat serializacji w zależności od wersji klienta.
Typowe pytania na rozmowach:
- Jak serializować tylko część danych?
- Jak sprawić, żeby właściwość kolekcji nie trafiła do JSON?
- Jak zmapować nazwy właściwości w C# i JSON, jeśli się różnią?
- Czy można ukryć pojedyncze elementy wewnątrz kolekcji przed serializacją (np. informacje poufne)?
Odpowiedzi kręcą się wokół poprawnego użycia atrybutów i parametrów serializacji: [JsonIgnore], [JsonPropertyName], opcji JsonSerializerOptions i przemyślanej pracy z zawartością kolekcji.
GO TO FULL VERSION