JSON to sposób, w jaki większość programów w C# rozmawia z webowymi API, przechowuje ustawienia i wymienia dane. Współczesny .NET czyta i zapisuje go przez System.Text.Json, który od .NET Core 3.0 jest częścią środowiska uruchomieniowego: żaden pakiet NuGet nie jest potrzebny. Głównym punktem wejścia jest statyczna klasa JsonSerializer, która zamienia obiekty na stringi JSON i z powrotem.
System.Text.Json jest wbudowany w .NET Core 3.0 i każde późniejsze wydanie (od .NET 5 do .NET 10). Projekty na .NET Framework 4.6.2 i nowszym albo na .NET Standard 2.0 też mogą go używać po zainstalowaniu pakietu NuGet System.Text.Json. Przykłady na tej stronie są pokazane jako zwykły kod z wynikiem w komentarzach. Aby uruchomić je w projekcie .NET, dodaj te dyrektywy using:
using System.Text.Json;
using System.Text.Json.Serialization;
Serializacja: obiekt do JSON
JsonSerializer.Serialize zapisuje każdą publiczną właściwość obiektu, używając nazw właściwości bez zmian:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public bool InStock { get; set; }
}
var lamp = new Product { Name = "Desk lamp", Price = 34.90m, Tags = { "home", "light" }, InStock = true };
string json = JsonSerializer.Serialize(lamp);
Console.WriteLine(json);
// {"Name":"Desk lamp","Price":34.90,"Tags":["home","light"],"InStock":true}
Kolekcje stają się tablicami, słowniki z kluczami typu string stają się obiektami ({"apples":3,"pears":5}), null zostaje null, a DateTime staje się stringiem ISO 8601 ("2026-03-01T14:30:00"). Dwa ustawienia domyślne często zaskakują:
- Pola są pomijane. Serializowane są tylko właściwości. Klasa z
public int X;serializuje się jako{}, chyba że ustawisz w opcjachIncludeFields = truealbo zamienisz pole na właściwość. - Enumy są liczbami.
OrderStatus.Shippedjest zapisywany jako1(wartość bazowa enuma). DodajJsonStringEnumConverter(niżej), aby zapisać"Shipped".
Deserializacja: JSON do obiektu
JsonSerializer.Deserialize<T> tworzy T i ustawia jego właściwości na podstawie JSON:
string json = "{\"Name\":\"Desk lamp\",\"Price\":34.90,\"Tags\":[\"home\",\"light\"]}";
Product p = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{p.Name} {p.Price} {p.Tags.Count}"); // Desk lamp 34.90 2
var many = JsonSerializer.Deserialize<List<Product>>("[{\"Name\":\"A\",\"Price\":1},{\"Name\":\"B\",\"Price\":2.5}]");
Console.WriteLine(many.Count); // 2
Właściwości JSON bez pasującej właściwości C# są ignorowane, a właściwości C# bez pasującego JSON zachowują wartości domyślne. Domyślnie żadna z tych sytuacji nie jest błędem.
Zasada, na której potyka się prawie każdy: dopasowanie nazw rozróżnia wielkość liter. Większość webowych API wysyła camelCase, a camelCase nie pasuje do właściwości w PascalCase:
string fromApi = "{\"name\":\"Mug\",\"price\":8.5}";
var a = JsonSerializer.Deserialize<Product>(fromApi);
Console.WriteLine($"[{a.Name}] {a.Price}"); // [] 0: nothing matched, no error
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var b = JsonSerializer.Deserialize<Product>(fromApi, options);
Console.WriteLine($"[{b.Name}] {b.Price}"); // [Mug] 8.5
new JsonSerializerOptions(JsonSerializerDefaults.Web) daje ustawienia, których używa ASP.NET Core: odczyt bez rozróżniania wielkości liter, zapis w camelCase i akceptowanie liczb zapisanych jako stringi w cudzysłowach.
Deserializacja potrzebuje sposobu na ustawienie każdej wartości: publicznego settera, akcesora init albo konstruktora, którego nazwy parametrów pasują do właściwości. Ta ostatnia zasada sprawia, że rekordy działają od razu:
public record Point(int X, int Y);
Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt); // Point { X = 1, Y = 2 }
Opcje: camelCase i wcięcia
JsonSerializerOptions kontroluje nazewnictwo, formatowanie i nie tylko. Utwórz jedną instancję i używaj jej ponownie: serializator zapamiętuje metadane dla każdej instancji opcji, więc tworzenie nowych opcji przy każdym wywołaniu jest wyraźnie wolniejsze.
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
Console.WriteLine(JsonSerializer.Serialize(lamp, Options));
// {
// "name": "Desk lamp",
// "price": 34.90,
// "tags": [
// "home",
// "light"
// ],
// "inStock": true
// }
Inne opcje, które warto znać: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, aby pomijać właściwości null, IncludeFields = true, NumberHandling, aby akceptować liczby zapisane jako stringi, oraz ReadCommentHandling = JsonCommentHandling.Skip razem z AllowTrailingCommas = true dla ręcznie edytowanych plików konfiguracyjnych. .NET 8 dodał JsonNamingPolicy.SnakeCaseLower dla API używających snake_case.
Atrybuty: zmiana nazwy, pomijanie, enumy jako stringi
Atrybuty na klasie kontrolują po jednej właściwości i mają pierwszeństwo przed opcjami:
public enum OrderStatus { Pending, Shipped }
public class Order
{
[JsonPropertyName("order_id")]
public int Id { get; set; }
public string Customer { get; set; }
[JsonIgnore]
public string InternalNote { get; set; } // never written or read
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Coupon { get; set; } // left out when null
[JsonConverter(typeof(JsonStringEnumConverter))]
public OrderStatus Status { get; set; }
public DateTime PlacedAt { get; set; }
}
var order = new Order
{
Id = 1042, Customer = "Ana", InternalNote = "vip",
Status = OrderStatus.Shipped, PlacedAt = new DateTime(2026, 3, 1, 14, 30, 0),
};
Console.WriteLine(JsonSerializer.Serialize(order));
// {"order_id":1042,"Customer":"Ana","Status":"Shipped","PlacedAt":"2026-03-01T14:30:00"}
Aby zapisywać każdy enum jako string zamiast oznaczać każdą właściwość, dodaj konwerter do opcji: options.Converters.Add(new JsonStringEnumConverter());.
Odczyt JSON bez klasy: JsonDocument i JsonNode
Gdy potrzebujesz tylko kilku wartości z dużej odpowiedzi albo jej kształt się zmienia, pomiń klasę. JsonDocument parsuje do drzewa wartości JsonElement tylko do odczytu:
string weather = "{\"city\":\"Lisbon\",\"current\":{\"temp\":21.5,\"conditions\":[\"sunny\",\"windy\"]},\"alerts\":null}";
using (JsonDocument doc = JsonDocument.Parse(weather))
{
JsonElement root = doc.RootElement;
Console.WriteLine(root.GetProperty("city").GetString()); // Lisbon
Console.WriteLine(root.GetProperty("current").GetProperty("temp").GetDecimal()); // 21.5
foreach (JsonElement c in root.GetProperty("current").GetProperty("conditions").EnumerateArray())
Console.WriteLine(c.GetString()); // sunny, windy
Console.WriteLine(root.TryGetProperty("humidity", out _)); // False
Console.WriteLine(root.GetProperty("alerts").ValueKind); // Null
}
GetProperty rzuca KeyNotFoundException przy brakującej nazwie, więc dla pól opcjonalnych używaj TryGetProperty. JsonDocument wypożycza pamięć z puli, dlatego zwalnia się go przez using.
Aby modyfikować JSON, użyj JsonNode z System.Text.Json.Nodes (.NET 6 i nowsze), który daje modyfikowalne drzewo z indekserami:
JsonNode node = JsonNode.Parse(weather);
Console.WriteLine((string)node["city"]); // Lisbon
node["current"]["temp"] = 23;
node["updated"] = true;
Console.WriteLine(node.ToJsonString());
// {"city":"Lisbon","current":{"temp":23,"conditions":["sunny","windy"]},"alerts":null,"updated":true}
Pliki i strumienie
JSON na dysku to string w pliku, więc metody plikowe łączą się bezpośrednio z serializatorem:
File.WriteAllText("settings.json", JsonSerializer.Serialize(settings, Options));
var loaded = JsonSerializer.Deserialize<Settings>(File.ReadAllText("settings.json"), Options);
// Same options both ways: camelCase names written with Options would not match on a default read.
Przy dużych plikach i treściach HTTP asynchroniczne przeciążenia dla strumieni pozwalają uniknąć budowania całego stringa w pamięci:
await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);
W ASP.NET Core i z HttpClient rzadko wywołujesz serializator samodzielnie: kontrolery automatycznie wiążą treści JSON, a httpClient.GetFromJsonAsync<Order>(url) (w System.Net.Http.Json) wykonuje żądanie i deserializację w jednym wywołaniu.
Błędy
Niepoprawny JSON albo wartość, której nie da się skonwertować na typ właściwości, rzuca JsonException. Jego komunikat podaje ścieżkę JSON i pozycję, co zwykle wystarcza, by znaleźć problem:
try
{
JsonSerializer.Deserialize<Product>("{\"Name\": \"Lamp\", \"Price\": \"cheap\"}");
}
catch (JsonException e)
{
Console.WriteLine(e.Message);
// The JSON value could not be converted to System.Decimal. Path: $.Price | LineNumber: 0 | BytePositionInLine: 33.
}
Deserialize zwraca null (a nie wyjątek), gdy tekst JSON to literał null, więc sprawdzaj wynik, gdy dane wejściowe pochodzą z zewnątrz.
Znaki zamienione na sekwencje ucieczki
Domyślnie serializator zamienia na sekwencje ucieczki znaki spoza ASCII i znaki niebezpieczne w HTML:
Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}
To poprawny JSON, który odczytuje się z powrotem jako oryginalny tekst; znaki są zamieniane po to, by wynik można było bezpiecznie wstawić do strony HTML. Dla plików czytanych przez ludzi ustaw w opcjach Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (z System.Text.Encodings.Web). Słowo "unsafe" dotyczy tylko osadzania wyniku w HTML.
System.Text.Json a Newtonsoft.Json
Newtonsoft.Json (Json.NET, klasa JsonConvert) przez dekadę był standardem i nadal jest wszędzie. Główne różnice:
| System.Text.Json | Newtonsoft.Json | |
|---|---|---|
| Dostępność | wbudowany w .NET Core 3.0+; pakiet NuGet dla .NET Framework 4.6.2+ | pakiet NuGet dla .NET Framework i .NET |
| Serializacja / deserializacja | JsonSerializer.Serialize(obj) / Deserialize<T>(json) | JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json) |
| Dopasowanie nazw | domyślnie rozróżnia wielkość liter | nie rozróżnia wielkości liter |
| Odczyt bez typów | JsonDocument, JsonNode | JObject, JToken, z zapytaniami JSONPath |
| Zmiana nazwy właściwości | [JsonPropertyName("x")] | [JsonProperty("x")] |
| Pobłażliwość | rygorystyczny: bez komentarzy, końcowych przecinków i liczb w cudzysłowach, chyba że je włączysz | domyślnie pobłażliwy |
| Wydajność | szybszy, mniej alokacji, generowanie kodu źródłowego dla trimmingu i AOT | wolniejszy, oparty na refleksji |
Atrybuty mają inne nazwy i namespace, więc przeniesienie projektu to praca typu "znajdź i zamień" plus testy dla bardziej rygorystycznego parsowania. W nowym kodzie .NET zacznij od System.Text.Json.
Generowanie kodu źródłowego (.NET 6+)
JsonSerializer zwykle analizuje twoje typy przez refleksję w trakcie działania. W aplikacjach przycinanych (trimming) albo kompilowanych z wyprzedzeniem (Native AOT, Blazor WebAssembly) generator kodu źródłowego pisze ten kod zamiast tego w czasie kompilacji:
[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }
string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);
Typowe błędy
- JSON w camelCase do właściwości w PascalCase z domyślnymi opcjami. Każda właściwość po cichu zostaje pusta. Użyj
PropertyNameCaseInsensitivealboJsonSerializerDefaults.Web. - Publiczne pola zamiast właściwości. Nie są serializowane, chyba że ustawiono
IncludeFields. - Właściwości bez settera. Właściwość tylko z getterem bez pasującego parametru konstruktora nie jest wypełniana przy deserializacji.
- Nowe
JsonSerializerOptionsprzy każdym wywołaniu. Używaj ponownie jednej statycznej instancji. - Pieniądze w liczbach zmiennoprzecinkowych.
doubleo wartości0.1 + 0.2serializuje się jako0.30000000000000004. Do kwot używajdecimal.
Najczęściej zadawane pytania
Jak zamienić obiekt na JSON w C#?
Wywołaj JsonSerializer.Serialize(obj) z System.Text.Json, który jest wbudowany w .NET Core 3.0 i nowsze, bez instalowania pakietu. Zapisuje każdą publiczną właściwość: {"Name":"Desk lamp","Price":34.90}. Przekaż new JsonSerializerOptions { WriteIndented = true }, aby dostać czytelny wynik, i PropertyNamingPolicy = JsonNamingPolicy.CamelCase dla nazw w camelCase.
Jak zamienić JSON na obiekt w C#?
var product = JsonSerializer.Deserialize<Product>(json); tworzy Product i wypełnia jego publiczne, zapisywalne właściwości na podstawie pasujących nazw w JSON. Dopasowanie domyślnie rozróżnia wielkość liter, więc JSON w camelCase zostawia puste właściwości w PascalCase, chyba że przekażesz PropertyNameCaseInsensitive = true albo new JsonSerializerOptions(JsonSerializerDefaults.Web). Niepoprawny JSON lub zły typ wartości rzuca JsonException.
Jak odczytać JSON bez tworzenia klasy w C#?
Użyj JsonDocument.Parse(json) i przechodź RootElement przez GetProperty("name"), GetString(), GetInt32() i EnumerateArray(); jest tylko do odczytu, szybki i trzeba go zwolnić. Dla JSON, który chcesz modyfikować, JsonNode.Parse(json) (.NET 6+) daje modyfikowalne drzewo: node["city"], przypisania i ToJsonString().
Czego używać: System.Text.Json czy Newtonsoft.Json?
W nowym kodzie na .NET Core 3.0 lub nowszym System.Text.Json: jest wbudowany, szybszy, mniej alokuje, a ASP.NET Core używa go domyślnie. Newtonsoft.Json (Json.NET) nadal jest popularny w projektach .NET Framework (gdzie System.Text.Json jest dostępny tylko jako pakiet NuGet), w kodzie zależnym od jego dodatkowych funkcji (zapytania JSONPath, bardzo pobłażliwe parsowanie, TypeNameHandling) i w dużych projektach już na nim zbudowanych.
Dlaczego System.Text.Json zamienia znaki takie jak é i < na sekwencje ucieczki?
Domyślny enkoder zamienia znaki spoza ASCII i znaki wrażliwe w HTML (<, >, &, ') na \uXXXX, żeby wynik można było bezpiecznie osadzić w HTML. To nadal poprawny JSON, który deserializuje się z powrotem do tego samego tekstu. Aby wynik był czytelny, ustaw w opcjach Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, ale tylko wtedy, gdy JSON nie trafia do HTML.