Підрозділ 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. Чим нижчий рівень — тим більший контроль і вища продуктивність, але й більше коду.

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-відповідей

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обмежує глибину вкладеності (захист від атак через переповнення стека)