CodeGym /Kursy /C# SELF /Zarządzanie serializacją kolekcji

Zarządzanie serializacją kolekcji

C# SELF
Poziom 46 , Lekcja 4
Dostępny

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
[JsonIgnore]
tak tak Ukryć listę lub pole w Book
[JsonPropertyName]
tak tak Zamienić Books → items lub Title → name
[JsonInclude]
tak tak Dołączyć prywatne właściwości do serializacji
[JsonConverter]
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.

1
Ankieta/quiz
Serializacja kolekcji, poziom 46, lekcja 4
Niedostępny
Serializacja kolekcji
Serializacja zagnieżdżonych i hierarchicznych obiektów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION