Menu

JSON в C#: сериализация и десериализация через System.Text.Json

Как работать с JSON в C# через System.Text.Json: JsonSerializer.Serialize и Deserialize, camelCase и форматированный вывод, атрибуты JsonPropertyName и JsonIgnore, перечисления в виде строк, чтение JSON без классов через JsonDocument и JsonNode, ошибки и сравнение с Newtonsoft.Json.

JSON это то, через что большинство программ на C# общаются с веб-API, хранят настройки и обмениваются данными. Современный .NET читает и пишет его через System.Text.Json, который входит в среду выполнения начиная с .NET Core 3.0: пакет NuGet не нужен. Его главная точка входа это статический класс JsonSerializer, который превращает объекты в строки JSON и обратно.

System.Text.Json встроен в .NET Core 3.0 и все следующие версии (с .NET 5 по .NET 10). Проекты на .NET Framework 4.6.2 и новее или на .NET Standard 2.0 тоже могут его использовать, установив пакет NuGet System.Text.Json. Примеры на этой странице показаны как обычный код с выводом в комментариях. Чтобы запустить их в проекте .NET, добавьте эти директивы using:

using System.Text.Json;
using System.Text.Json.Serialization;

Сериализация: объект в JSON

JsonSerializer.Serialize записывает каждое открытое свойство объекта, используя имена свойств как есть:

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}

Коллекции становятся массивами, словари со строковыми ключами становятся объектами ({"apples":3,"pears":5}), null остаётся null, а DateTime становится строкой ISO 8601 ("2026-03-01T14:30:00"). Два поведения по умолчанию застают врасплох:

  • Поля пропускаются. Сериализуются только свойства. Класс с public int X; сериализуется как {}, если не задать в параметрах IncludeFields = true или не превратить поле в свойство.
  • Перечисления это числа. OrderStatus.Shipped записывается как 1 (базовое значение перечисления). Добавьте JsonStringEnumConverter (ниже), чтобы записывать "Shipped".

Десериализация: JSON в объект

JsonSerializer.Deserialize<T> создаёт T и задаёт его свойства из 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

Свойства JSON без соответствующего свойства C# игнорируются, а свойства C# без соответствующего JSON сохраняют значения по умолчанию. По умолчанию ни то, ни другое не является ошибкой.

Правило, на котором спотыкаются почти все: сопоставление имён учитывает регистр. Большинство веб-API присылают camelCase, а camelCase не совпадает со свойствами в 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) даёт настройки, которые использует ASP.NET Core: чтение без учёта регистра, запись в camelCase и приём чисел, записанных строками в кавычках.

Десериализации нужен способ задать каждое значение: открытый сеттер, метод доступа init или конструктор, имена параметров которого совпадают со свойствами. Последнее правило объясняет, почему записи работают сразу:

public record Point(int X, int Y);

Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt);   // Point { X = 1, Y = 2 }

Параметры: camelCase и форматированный вывод

JsonSerializerOptions управляет именованием, форматированием и многим другим. Создайте один экземпляр и используйте его повторно: сериализатор кэширует метаданные для каждого экземпляра параметров, поэтому создание новых параметров при каждом вызове заметно медленнее.

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
// }

Другие параметры, которые стоит знать: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, чтобы пропускать свойства со значением null, IncludeFields = true, NumberHandling, чтобы принимать числа, записанные строками, и ReadCommentHandling = JsonCommentHandling.Skip вместе с AllowTrailingCommas = true для файлов конфигурации, которые правят вручную. .NET 8 добавил JsonNamingPolicy.SnakeCaseLower для API, использующих snake_case.

Атрибуты: переименование, пропуск, перечисления строками

Атрибуты в классе управляют отдельными свойствами и имеют приоритет над параметрами:

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"}

Чтобы записывать каждое перечисление строкой, не помечая каждое свойство, добавьте преобразователь в параметры: options.Converters.Add(new JsonStringEnumConverter());.

Чтение JSON без класса: JsonDocument и JsonNode

Когда из большого ответа нужно лишь несколько значений или его форма меняется, обойдитесь без класса. JsonDocument разбирает JSON в дерево значений JsonElement только для чтения:

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 выбрасывает KeyNotFoundException для отсутствующего имени, поэтому для необязательных полей используйте TryGetProperty. JsonDocument арендует память из пула, поэтому его освобождают через using.

Чтобы изменять JSON, используйте JsonNode из System.Text.Json.Nodes (.NET 6 и новее), который даёт изменяемое дерево с индексаторами:

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}

Файлы и потоки

JSON на диске это строка в файле, поэтому методы работы с файлами напрямую сочетаются с сериализатором:

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.

Для больших файлов и тел HTTP асинхронные перегрузки для потоков избегают построения всей строки в памяти:

await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);

В ASP.NET Core и с HttpClient вы редко вызываете сериализатор сами: контроллеры автоматически привязывают тела JSON, а httpClient.GetFromJsonAsync<Order>(url) (из System.Net.Http.Json) выполняет запрос и десериализацию одним вызовом.

Ошибки

Некорректный JSON или значение, которое нельзя преобразовать к типу свойства, выбрасывают JsonException. Его сообщение называет путь в JSON и позицию, и обычно этого достаточно, чтобы найти проблему:

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 возвращает null (а не исключение), когда текст JSON это литерал null, поэтому проверяйте результат, когда входные данные приходят извне.

Экранированные символы в выводе

По умолчанию сериализатор экранирует символы вне ASCII и символы, небезопасные внутри HTML:

Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}

Это корректный JSON, и он читается обратно в исходный текст; экранирование нужно, чтобы вывод можно было безопасно вставить в HTML-страницу. Для файлов, которые читают люди, задайте в параметрах Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (из System.Text.Encodings.Web). Слово «unsafe» относится только к встраиванию результата в HTML.

System.Text.Json и Newtonsoft.Json

Newtonsoft.Json (Json.NET, класс JsonConvert) десять лет был стандартом и до сих пор встречается повсюду. Основные различия:

System.Text.JsonNewtonsoft.Json
Доступностьвстроен в .NET Core 3.0+; пакет NuGet для .NET Framework 4.6.2+пакет NuGet для .NET Framework и .NET
Сериализация / десериализацияJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
Сопоставление имёнпо умолчанию с учётом регистрабез учёта регистра
Чтение без типовJsonDocument, JsonNodeJObject, JToken, с запросами JSONPath
Переименование свойства[JsonPropertyName("x")][JsonProperty("x")]
Снисходительностьстрогий: без комментариев, лишних запятых и чисел в кавычках, если не включитьснисходительный по умолчанию
Производительностьбыстрее, меньше выделений, генерация исходного кода для обрезки и AOTмедленнее, на основе рефлексии

У атрибутов разные имена и пространства имён, поэтому перевод кодовой базы это работа поиска и замены плюс тесты на более строгий разбор. Для нового кода на .NET начинайте с System.Text.Json.

Генерация исходного кода (.NET 6+)

JsonSerializer обычно исследует ваши типы через рефлексию во время выполнения. Для приложений, которые обрезаются или компилируются заранее (Native AOT, Blazor WebAssembly), генератор исходного кода пишет этот код при компиляции:

[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }

string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);

Частые ошибки

  • JSON в camelCase в свойства PascalCase с параметрами по умолчанию. Все свойства молча остаются пустыми. Используйте PropertyNameCaseInsensitive или JsonSerializerDefaults.Web.
  • Открытые поля вместо свойств. Они не сериализуются, если не задан IncludeFields.
  • Свойства без сеттера. Свойство только для чтения без соответствующего параметра конструктора не заполняется при десериализации.
  • Новый JsonSerializerOptions на каждый вызов. Используйте повторно один статический экземпляр.
  • Деньги в числах с плавающей точкой. double, равный 0.1 + 0.2, сериализуется как 0.30000000000000004. Для сумм используйте decimal.

Часто задаваемые вопросы

Как преобразовать объект в JSON в C#?

Вызовите JsonSerializer.Serialize(obj) из System.Text.Json, который встроен в .NET Core 3.0 и новее, и никакой пакет устанавливать не нужно. Он записывает каждое открытое свойство: {"Name":"Desk lamp","Price":34.90}. Передайте new JsonSerializerOptions { WriteIndented = true } для читаемого вывода и PropertyNamingPolicy = JsonNamingPolicy.CamelCase для имён в camelCase.

Как преобразовать JSON в объект в C#?

var product = JsonSerializer.Deserialize<Product>(json); создаёт Product и заполняет его открытые задаваемые свойства из совпадающих имён JSON. Сопоставление по умолчанию учитывает регистр, поэтому JSON в camelCase оставляет свойства в PascalCase пустыми, если не передать PropertyNameCaseInsensitive = true или new JsonSerializerOptions(JsonSerializerDefaults.Web). Некорректный JSON или значение неверного типа выбрасывают JsonException.

Как прочитать JSON без создания класса в C#?

Используйте JsonDocument.Parse(json) и обходите RootElement через GetProperty("name"), GetString(), GetInt32() и EnumerateArray(); он только для чтения, быстрый и требует освобождения. Для JSON, который нужно изменять, JsonNode.Parse(json) (.NET 6+) даёт изменяемое дерево: node["city"], присваивания и ToJsonString().

Что использовать, System.Text.Json или Newtonsoft.Json?

Для нового кода на .NET Core 3.0 или новее System.Text.Json: он встроен, быстрее, меньше выделяет памяти, и ASP.NET Core использует его по умолчанию. Newtonsoft.Json (Json.NET) по-прежнему распространён в проектах на .NET Framework (где System.Text.Json доступен только как пакет NuGet), в коде, зависящем от его дополнительных возможностей (запросы JSONPath, очень снисходительный разбор, TypeNameHandling), и в больших кодовых базах, уже построенных на нём.

Почему System.Text.Json экранирует символы вроде é и <?

Кодировщик по умолчанию экранирует символы вне ASCII и символы, опасные для HTML (<, >, &, '), как \uXXXX, чтобы вывод можно было безопасно встроить в HTML. Это по-прежнему корректный JSON, и он десериализуется обратно в тот же текст. Для читаемого вывода задайте в параметрах Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, но только когда JSON не записывается в HTML.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ