OOP Course
Сьогодні

Підрозділ 19.1

System.Text.Json — поглиблено

19.1. System.Text.Json — поглиблено У розділі 18.6 ми розглянули базові операції JsonSerializer.Serialize / Deserialize — достатньо для більшості задач. Але реальні системи стикаються з ситуаціями, де базового

19.1. System.Text.Json — поглиблено

У розділі 18.6 ми розглянули базові операції JsonSerializer.Serialize / Deserialize — достатньо для більшості задач. Але реальні системи стикаються з ситуаціями, де базового API не вистачає: схема JSON невідома заздалегідь, потрібна власна логіка серіалізації для специфічного типу, дані надходять як потік з HTTP і не варто буферизувати їх у рядок, або ієрархія класів потребує поліморфної серіалізації.

System.Text.Json надає три рівні API: від зручного JsonSerializer до низькорівневих JsonDocument/Utf8JsonReader. Чим нижчий рівень — тим більший контроль і вища продуктивність, але й більше коду.

Три рівні API System.Text.Json

JsonDocument та JsonElement — DOM без десеріалізації

JsonDocument — це легка DOM-модель для JSON, яка дозволяє читати та навігувати по JSON-документу без прив'язки до конкретного C#-класу. Корисно, коли схема документа невідома або змінюється — наприклад, відповідь зовнішнього API, налаштування плагінів, гетерогенні медичні записи.

JsonDocument реалізує IDisposable — завжди використовуйте using. Внутрішньо він рентить пам'ять з пулу (ArrayPool<byte>), і Dispose повертає її назад. Без Dispose пам'ять буде затримана до наступного GC.

TryGetProperty та перевірка типів

JsonValueKind — перелік, що описує JSON-тип вузла: Object, Array, String, Number, True, False, Null, Undefined. Перевіряйте ValueKind перед читанням, якщо схема документа не гарантована.

Utf8JsonWriter — швидкісний запис JSON

Utf8JsonWriter — найнижчий рівень запису JSON. Він записує безпосередньо в UTF-8 байти без проміжного рядкового представлення — це найшвидший можливий спосіб генерації JSON у .NET:

Utf8JsonWriter застосовується у бібліотеках і фреймворках, де продуктивність критична — наприклад, ASP.NET Core використовує його внутрішньо для серіалізації HTTP-відповідей. Для звичайного застосунку достатньо JsonSerializer.

Кастомні конвертери: JsonConverter

Стандартна серіалізація не завжди підходить для специфічних типів. JsonConverter<T> дозволяє повністю контролювати, як конкретний тип читається з JSON і записується в JSON:

Конвертер можна зареєструвати глобально через options.Converters.Add(new NormalRangeConverter()) замість атрибута на кожному полі — тоді він спрацює для всіх властивостей цього типу в усій програмі.

Поліморфна серіалізація: JsonDerivedType

Коли в полі може бути один з кількох підтипів, JsonSerializer за замовчуванням серіалізує лише властивості базового типу і при десеріалізації не знає, який підтип відновити. Атрибути [JsonPolymorphic] і [JsonDerivedType] вирішують це:

$type — дискримінатор типу, який JsonSerializer автоматично додає при записі і читає при десеріалізації. Ім'я поля (TypeDiscriminatorPropertyName) можна задати довільним — часто використовують "type", "kind", "$type".

Async серіалізація: SerializeAsync та DeserializeAsync

Для роботи з HTTP-потоками та великими файлами є async-версії, що не блокують потік:

SerializeAsync / DeserializeAsync — особливо важливі у ASP.NET Core: при відповіді на HTTP-запит серіалізація відбувається напряму у HttpResponse.Body-потік без буферизації всього JSON у пам'яті. Для консольних застосунків або Desktop різниця мінімальна, але для веб-сервісів під навантаженням — суттєва.

Практичний сценарій: агрегація гетерогенних JSON-відповідей

Кастомний JsonConverter<T> — схема роботи

JSON Source Generators — серіалізація без рефлексії

Стандартний JsonSerializer використовує рефлексію (reflection) під час виконання: він динамічно аналізує типи, знаходить властивості та генерує відповідний код. Це зручно, але має три недоліки:

  • Продуктивність: рефлексія повільніша за статично сгенерований код
  • NativeAOT / trimming: рефлексія несумісна з AOT-компіляцією та агресивним видаленням невикористаного коду
  • Розмір застосунку: рефлексивний код не trimmed компілятором

JSON Source Generators (.NET 6+) вирішують усе це: ви оголошуєте JsonSerializerContext — і компілятор генерує весь серіалізаційний код на етапі компіляції, без рефлексії під час виконання.

Синтаксис MedicalJsonContext.Default.PatientRecord — це сгенерований JsonTypeInfo<PatientRecord>, який передається замість опцій. Компілятор знаходить атрибут [JsonSerializable(typeof(PatientRecord))] і генерує весь необхідний код у часткому класі MedicalJsonContext.

Режим Продуктивність NativeAOT Зручність
Reflection (за замовчуванням) Помірна ★★★★★
Source Generators Швидший (~2–3x) ★★★★

Для нових проектів на .NET 7+ та особливо для AOT-сценаріїв (мобільні застосунки, мікросервіси з швидким стартом) рекомендується Source Generators.

Антипатерн: небезпечна десеріалізація

Не десеріалізуйте довільний JSON без валідації типу і вмісту. Декілька прикладів небезпечних паттернів:

Правила безпечної десеріалізації:

  • Окремі DTO для вхідних і вихідних даних — Request не містить полів безпеки, Response не містить внутрішніх полів
  • [JsonIgnore] для полів, що ніколи не мають надходити від клієнта
  • Валідація після десеріалізації: перевіряйте діапазони значень, обов'язкові поля, допустимі enum-значення
  • MaxDepth у JsonSerializerOptions обмежує глибину вкладеності (захист від атак через переповнення стека)
Розроблено Tomka Yurii · © 2026 ·