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.Json | Newtonsoft.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, JsonNode | JObject, 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.