Menu

JSON w C#: serializacja i deserializacja z System.Text.Json

Jak pracować z JSON w C# przez System.Text.Json: JsonSerializer.Serialize i Deserialize, camelCase i wcięcia, atrybuty takie jak JsonPropertyName i JsonIgnore, enumy jako stringi, odczyt JSON bez klas przez JsonDocument i JsonNode, błędy oraz porównanie z Newtonsoft.Json.

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 opcjach IncludeFields = true albo zamienisz pole na właściwość.
  • Enumy są liczbami. OrderStatus.Shipped jest zapisywany jako 1 (wartość bazowa enuma). Dodaj JsonStringEnumConverter (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.JsonNewtonsoft.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 / deserializacjaJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
Dopasowanie nazwdomyślnie rozróżnia wielkość liternie rozróżnia wielkości liter
Odczyt bez typówJsonDocument, JsonNodeJObject, 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łączyszdomyślnie pobłażliwy
Wydajnośćszybszy, mniej alokacji, generowanie kodu źródłowego dla trimmingu i AOTwolniejszy, 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 PropertyNameCaseInsensitive albo JsonSerializerDefaults.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 JsonSerializerOptions przy każdym wywołaniu. Używaj ponownie jednej statycznej instancji.
  • Pieniądze w liczbach zmiennoprzecinkowych. double o wartości 0.1 + 0.2 serializuje się jako 0.30000000000000004. Do kwot używaj decimal.

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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ