System.Text.Json — serializacja i deserializacja 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.
JsonSerializerOptionsjest kosztowny w tworzeniu i buforuje metadane typów. Twórz jedną instancję i używaj jej wielokrotnie (np.static readonly), nienewprzy 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 kruche1 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.Json | Newtonsoft.Json | |
|---|---|---|
| Dostępność | wbudowany w .NET | pakiet NuGet |
| Wydajność | ✅ szybszy, mniej alokacji | wolniejszy |
| Domyślny w ASP.NET Core | ✅ tak (od .NET Core 3.0) | trzeba włączyć ręcznie |
| Bogactwo funkcji | podstawy + rośnie | ✅ bardzo bogaty (dojrzały) |
| Nowy projekt | ✅ domyślny wybór | tylko 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 = CamelCasedla 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, nieneww 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.
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →