Сериализация: System.Text.Json и Newtonsoft
↑ 2.1 C♯: продвинутый язык · ← Предыдущая · → Следующая
Зачем это на собесе
JSON — формат обмена почти в каждом API. Спросят, чем
System.Text.Jsonотличается от Newtonsoft, как настроить имена и регистр, как сериализовать enum и даты, как работать с большими JSON потоково, что такое source generation и чем опасна десериализация типов из данных.
Объяснение
System.Text.Json (STJ) — встроенный
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) // camelCase, регистронезависимость при чтении
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }, // enum как строки
};
string json = JsonSerializer.Serialize(patient, options);
var back = JsonSerializer.Deserialize<PatientDto>(json, options);Атрибуты:
public record PatientDto(
[property: JsonPropertyName("last_name")] string LastName,
[property: JsonIgnore] string Secret,
DateOnly BirthDate);
public class Item
{
[JsonRequired] public string Code { get; set; } = ""; // обязательное поле (или C# required)
[JsonPropertyOrder(1)] public int Qty { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))] public Status Status { get; set; }
}В ASP.NET Core настройки по умолчанию — JsonSerializerDefaults.Web. Свои: builder.Services.ConfigureHttpJsonOptions(o => ...) (Minimal API) или AddJsonOptions (MVC).
Newtonsoft.Json (Json.NET)
Исторически самая популярная библиотека, богаче по возможностям: JObject/JToken, [JsonProperty], TypeNameHandling, NullValueHandling, гибкие конвертеры, поддержка ISerializable, IDictionary со сложными ключами, ReferenceLoopHandling из коробки.
var s = JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
});Сравнение
| System.Text.Json | Newtonsoft.Json | |
|---|---|---|
| Скорость и память | выше, меньше аллокаций (UTF-8, Span) | ниже |
| Возможности | строже, меньше «магии», быстро развивается | богаче, более терпим к формату |
| Регистр имён | точное сравнение (в Web-режиме нечувствителен) | нечувствителен |
| Циклические ссылки | ReferenceHandler.Preserve / IgnoreCycles | ReferenceLoopHandling |
| Динамика | JsonNode, JsonDocument | JObject, dynamic |
| Source generation / AOT | да | нет |
| Полиморфизм | [JsonDerivedType] (.NET 7+) | TypeNameHandling (рискованно) |
Выбор: STJ по умолчанию; Newtonsoft — когда нужна совместимость, сложные конвертеры или устаревшие сценарии (некоторые библиотеки всё ещё требуют).
Даты, числа, enum
DateTime/DateTimeOffsetпишутся в ISO 8601.DateOnly/TimeOnlyподдерживаются (.NET 7+).decimalсохраняет точность (число в JSON); для больших чисел в JS-клиенте возможна потеря точности (long> 2^53) → передавайте строкой (JsonNumberHandling.WriteAsString).- Enum по умолчанию числом. Для устойчивого API — строки (
JsonStringEnumConverter) и явные значения, чтобы порядок не менялся.
Полиморфизм
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(Card), "card")]
[JsonDerivedType(typeof(Sbp), "sbp")]
public abstract record Payment(decimal Amount);
public sealed record Card(decimal Amount, string Last4) : Payment(Amount);Потоковая работа и низкий уровень
JsonSerializer.DeserializeAsyncEnumerable<T>(stream)— читает массив элементов по одному без загрузки всего в память.Utf8JsonReader/Utf8JsonWriter— чтение и запись токенов без аллокаций.JsonDocument— только чтение с деревом DOM (требуетDispose),JsonNode— изменяемое дерево.
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<Item>(stream, options, ct)) { }Source generation
[JsonSerializable(typeof(PatientDto))]
[JsonSerializable(typeof(List<PatientDto>))]
public partial class AppJsonContext : JsonSerializerContext;
var json = JsonSerializer.Serialize(patient, AppJsonContext.Default.PatientDto);Быстрее старт и меньше памяти, совместимо с trimming и Native AOT, нет рефлексии в рантайме.
Нюансы и подводные камни
- Циклические ссылки (
Parent.Children[0].Parent): STJ бросает исключение; включитеReferenceHandler.IgnoreCyclesили разрывайте цикл в DTO. - Регистр: без
Web-режимаlastNameне десериализуется вLastName(сравнение чувствительно). - Неизвестные поля по умолчанию игнорируются; строгий режим —
UnmappedMemberHandling.Disallow. - Null в не-nullable свойстве: NRT не защищает; нужны
requiredи валидация. - Приватные сеттеры/конструкторы: STJ использует публичные сеттеры и конструктор с параметрами (для record удобно). Нужен
[JsonConstructor]при нескольких конструкторах. - Безопасность:
TypeNameHandlingу Newtonsoft в сочетании с недоверенным вводом — известная уязвимость (выполнение произвольных типов). Не включайте для внешних данных, используйте белые списки или[JsonDerivedType]. - Большие JSON (сотни МБ) не читайте через
ReadAsStringAsync: потоково и без буферизации всего. - Смена сериализатора в API меняет формат (даты, enum, null) и ломает клиентов: фиксируйте контракт тестами.
Практика
- Настроить
JsonSerializerOptionsдля API: camelCase, enum строками, игнор null,DateOnly. - Написать конвертер для типа-обёртки
Money. - Прочитать файл на 500 МБ через
DeserializeAsyncEnumerableи замерить память.
Вопросы с ответами
Чем System.Text.Json отличается от Newtonsoft.Json?
STJ встроен, быстрее и экономичнее по памяти (работа с UTF-8 и Span), поддерживает source generation и AOT, но строже и изначально имел меньше возможностей. Newtonsoft богаче по функциям и терпимее к формату, но медленнее и без AOT.
Как настроить camelCase и enum как строки в ASP.NET Core?
По умолчанию STJ в ASP.NET Core использует Web-настройки (camelCase). Enum строками:
Converters.Add(new JsonStringEnumConverter())вConfigureHttpJsonOptionsилиAddJsonOptions.
Как обработать огромный JSON без загрузки в память?
Потоково:
DeserializeAsyncEnumerable<T>для массивов илиUtf8JsonReaderдля токенов; читать напрямую изStream.
Что такое JSON source generation и зачем оно?
Генерация сериализаторов во время компиляции через
JsonSerializerContext: нет рефлексии в рантайме, быстрее старт, меньше памяти, совместимость с trimming и Native AOT.
Чем опасен TypeNameHandling в Newtonsoft?
Тип для создания берётся из самих данных. Злоумышленник может указать опасный тип и получить выполнение кода при десериализации. Для недоверенных данных выключать или использовать белый список типов.
Связанные темы
- Предыдущая: Source generators и Roslyn (как в analytics_generator) · Следующая: [[be-2-1-12-chto-novogo-v-csharp-10-13|Что нового в C# 10–13 и .NET 8–9]]
- Source generators: Source generators и Roslyn (как в analytics_generator)
- Span и Utf8JsonReader: Span и Memory: stackalloc, ref struct
- Проектирование REST API: 5.1 Проектирование REST API
- JSON и fetch на фронтенде: 5.4 Работа с API: REST, GraphQL, WebSocket