🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

System.Text.Json — serializacja i deserializacja w .NET

System.Text.Json — serializacja obiektu C# do JSON (camelCase) i deserializacja z powrotem w .NET

Każde REST API, każdy plik appsettings.json, każda odpowiedź z zewnętrznego serwisu — to JSON. W .NET domyślnym narzędziem do zamiany obiektów C# na JSON i z powrotem jest System.Text.Json (wbudowane od .NET Core 3.0, nie mylić ze starszym Newtonsoft.Json). Używasz go pośrednio w każdym API, dopóki wszystko działa. Problem zaczyna się, gdy właściwości wracają jako null, enum serializuje się jako liczba, a data w złym formacie — i wtedy musisz wiedzieć, co ta biblioteka naprawdę robi.

Serializacja i deserializacja w dwóch zdaniach

Serializacja to zamiana obiektu C# na tekst JSON. Deserializacja to kierunek odwrotny — z JSON-a budujesz obiekt. System.Text.Json robi oba przez statyczną klasę JsonSerializer:

public record Produkt(int Id, string Nazwa, decimal Cena);

var produkt = new Produkt(1, "Klawiatura", 149.99m);

string json = JsonSerializer.Serialize(produkt);
// {"Id":1,"Nazwa":"Klawiatura","Cena":149.99}

Produkt z = JsonSerializer.Deserialize<Produkt>(json)!;

To wszystko, czego potrzebujesz w 90% przypadków. Reszta postu to JsonSerializerOptions — konfiguracja, która decyduje o pozostałych 10%, w których większość ludzi się potyka.

JsonSerializerOptions — sterowanie wynikiem

Domyślne zachowanie rzadko pasuje w praktyce. Opcje przekazujesz jako drugi argument:

var opcje = new JsonSerializerOptions
{
    WriteIndented = true,                                 // czytelne formatowanie
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,    // "nazwa" zamiast "Nazwa"
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull  // pomiń pola null
};

string json = JsonSerializer.Serialize(produkt, opcje);
// {
//   "id": 1,
//   "nazwa": "Klawiatura",
//   "cena": 149.99
// }

PropertyNamingPolicy = CamelCase to najważniejsza opcja: konwencja C# to PascalCase (Nazwa), a konwencja JSON i JavaScriptu to camelCase (nazwa). Bez niej Twoje API wysyła Nazwa, a frontend oczekuje nazwa — i dostaje undefined.

JsonSerializerOptions jest kosztowny w tworzeniu i buforuje metadane typów. Twórz jedną instancję i używaj jej wielokrotnie (np. static readonly), nie new przy każdym wywołaniu — inaczej tracisz wydajność, dla której System.Text.Json powstał.

Kontrola nad pojedynczymi właściwościami

Atrybuty na właściwościach nadpisują globalne opcje:

public class Uzytkownik
{
    [JsonPropertyName("user_id")]     // inna nazwa w JSON
    public int Id { get; set; }

    public string Email { get; set; } = "";

    [JsonIgnore]                       // nigdy nie serializuj
    public string HasloHash { get; set; } = "";
}

[JsonPropertyName] mapuje właściwość na konkretną nazwę w JSON (przydatne przy cudzym API, które używa user_id). [JsonIgnore] wyklucza pole całkowicie — obowiązkowe dla haseł, tokenów i pól wewnętrznych, których nie chcesz wysyłać na zewnątrz.

Enumy — domyślnie liczba, prawie zawsze chcesz string

Domyślnie System.Text.Json serializuje enum jako liczbę:

public enum Status { Nowy, Wyslany, Dostarczony }
// domyślnie: {"status":1}  — nieczytelne i kruche

1 nic nie mówi czytelnikowi API i psuje się przy zmianie kolejności wartości enuma. Rozwiązanie to JsonStringEnumConverter:

var opcje = new JsonSerializerOptions
{
    Converters = { new JsonStringEnumConverter() }
};
// teraz: {"status":"Wyslany"}

Własny konwerter — gdy wbudowane nie wystarcza

Gdy typ wymaga nietypowej reprezentacji (własny format daty, wartość jako inny typ), piszesz JsonConverter<T>:

public class DataConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type t, JsonSerializerOptions o)
        => DateTime.ParseExact(reader.GetString()!, "dd.MM.yyyy", null);

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions o)
        => writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
}

Rejestrujesz go w Converters tak samo jak JsonStringEnumConverter. To ta sama mechanika, którą framework stosuje dla typów wbudowanych.

System.Text.Json vs Newtonsoft.Json

W starszych projektach spotkasz Newtonsoft.Json (Json.NET). Kiedy który:

System.Text.JsonNewtonsoft.Json
Dostępnośćwbudowany w .NETpakiet NuGet
Wydajność✅ szybszy, mniej alokacjiwolniejszy
Domyślny w ASP.NET Core✅ tak (od .NET Core 3.0)trzeba włączyć ręcznie
Bogactwo funkcjipodstawy + rośnie✅ bardzo bogaty (dojrzały)
Nowy projekt✅ domyślny wybórtylko gdy brakuje funkcji

Dla nowego kodu zaczynasz od System.Text.Json. Po Newtonsoft sięgasz świadomie, gdy potrzebujesz funkcji, której wbudowana biblioteka jeszcze nie ma.

Pułapki rekrutacyjne

Pułapka #1 — wielkość liter przy deserializacji. Domyślnie dopasowanie nazw jest wrażliwe na wielkość liter. JSON {"nazwa":"..."} (camelCase) nie wypełni właściwości Nazwa (PascalCase) — pole zostanie null/domyślne, bez błędu. To najczęstsza przyczyna „deserializacja zwraca pusty obiekt”. Rozwiązanie: PropertyNameCaseInsensitive = true albo spójna PropertyNamingPolicy.

Pułapka #2 — brak konstruktora bezparametrowego. Do deserializacji klasy System.Text.Json potrzebuje albo konstruktora bezparametrowego + settery, albo konstruktora, którego parametry pasują nazwami do właściwości. Rekordy pozycyjne (record Produkt(int Id, ...)) działają, bo mają taki konstruktor — ale klasa z jedynym konstruktorem o niepasujących parametrach rzuci wyjątek. Warto rozumieć różnice records/structs/classes w tym kontekście.

Pułapka #3 — pola nie są serializowane. Domyślnie serializowane są tylko publiczne właściwości ({ get; set; }), nie pola (public int x;). Publiczne pole po prostu zniknie z JSON-a. Używaj właściwości albo włącz IncludeFields = true.

Pułapka #4 — cykliczne referencje. Obiekt Zamowienie z referencją do Klient, który ma listę Zamowienia z powrotem do tego zamówienia — serializacja wpada w nieskończoną pętlę i rzuca wyjątek. Rozwiązania: ReferenceHandler.IgnoreCycles, DTO bez wzajemnych referencji (najczystsze) albo [JsonIgnore] na właściwości nawigacyjnej. To realny problem, gdy próbujesz serializować encje EF Core wprost — kolejny powód, by API zwracało DTO, nie encje.

Pułapka #5 — JsonSerializerOptions tworzony w pętli. Wspomniany wyżej koszt: nowa instancja opcji przy każdym żądaniu buduje metadane typów od nowa i kasuje bufor. W ASP.NET Core konfigurujesz je raz (globalnie albo jako static readonly), nie per wywołanie.

Checklist — JSON w .NET bez niespodzianek

  • PropertyNamingPolicy = CamelCase dla API zgodnego z frontendem
  • enumy przez JsonStringEnumConverter (string, nie liczba)
  • [JsonIgnore] na hasłach, tokenach, polach wewnętrznych
  • deserializacja: spójne nazewnictwo albo PropertyNameCaseInsensitive
  • serializujesz DTO, nie encje (cykle, wyciek pól)
  • jedna instancja JsonSerializerOptions, nie new w pętli

Podsumowanie

System.Text.Json to wbudowane, szybkie i domyślne w .NET narzędzie do serializacji: JsonSerializer.Serialize zamienia obiekt na JSON, Deserialize<T> odtwarza obiekt z tekstu, a JsonSerializerOptions steruje resztą — nazewnictwem (CamelCase), obsługą null, enumami i własnymi konwerterami. Większość realnych problemów to nie brak funkcji, lecz domyślne ustawienia: wrażliwość na wielkość liter (puste obiekty po deserializacji), enum jako liczba, ignorowane pola i cykliczne referencje przy serializacji encji. Kto zna te domyślne zachowania i wie, którą opcją je zmienić, przestaje traktować serializację jak magię — a zaczyna jak przewidywalne narzędzie.

Co dalej

JSON to język, którym mówi Twoje REST API w ASP.NET Core — tam System.Text.Json działa pod spodem przy każdym żądaniu i odpowiedzi. Ten sam format napędza konfigurację (appsettings.json). A jeśli pracujesz ze starszym formatem danych, zobacz pokrewny temat LINQ to XML — ta sama idea (obiekt ↔ tekst), inny format.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →