Net-Base Журнал

13.08.2026

JSON у Delphi: Швидкий потоковий парсер з System.JSON + підводні камені UTF-8 при обробці спеціальних символів

Коли JSON-пейлоади в Delphi надходять безпосередньо з потоку, «просто й швидко розпарсити» може миттєво перетворитися на проблему в продакшені: великий обсяг пам’яті, періодичні помилки розбору та спотворені умляути. Ця практична стаття показує, як за допомогою System.JSON реалізувати швидкий парсер потокових даних...

13.08.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

«JSON in Delphi» звучить як вирішена проблема: System.JSON уже в комплекті, REST-виклики повертають текст — і все. На практиці ж реальні помилки виникають там, де JSON не представлений у зручному рядку, а як потік: HTTP-Response-Stream, Datei-Stream, Named Pipe, Message-Queue або великий BLOB у базі даних. Тут сходяться три часто недооцінювані речі: поведінка пам’яті, кодування символів (особливо UTF-8) і крайові випадки зі спеціальними символами.

У цій статті показано чистий, швидкий підхід до парсингу JSON із TStream без зайвих копій — і головне, без типових підводних каменів UTF-8, коли символи з діакритикою «псуються» або парсери випадково завершуються з криптичними повідомленнями. Фокус на впливах для експлуатації та стабільності інтерфейсів: відтворюване налагодження, чіткі межі підходу та критерії, коли витрати на реалізацію дійсно виправдані.

Чому потоки поводяться інакше при парсингу JSON у Delphi

Поки документ JSON невеликий, підхід «зчитати потік у рядок, потім парсити» зручний. Але при певному розмірі корисного навантаження (типово: великі списки, звіти, дані синхронізації, експорти логів) це стає затратним:

  • Дублювання пам’яті: ви читаєте байти в буфер, перетворюєте їх у Unicode-рядок (Delphi-рядок = UTF-16), парсер всередині створює додаткові структури. Це може означати кілька копій одночасно.
  • Тиск на GC/кучу: багато тимчасових рядків і JSON-значень збільшують фрагментацію та накладні витрати на виділення пам’яті, особливо в довготривалих процесах (Services, Worker, Import-Jobs).
  • Картинка помилки стає неясною: якщо під час зчитування зроблені неправильні припущення щодо кодування, парсер JSON побачить лише «дивні символи» або несподівані керуючі байти.

Важливо чітко розділяти: JSON формально — Unicode, по каналу він майже завжди UTF-8. Delphi же працює всередині з UTF-16. Перехід від байтів (Stream) до символів (String) — це саме те місце, де виникають проблеми зі спеціальними символами, а не в самому JSON.

System.JSON: що робить добре — і на що тобі слід звернути увагу

System.JSON у Delphi — стандарт для DOM-орієнтованого JSON: ви отримуєте об’єктну модель (TJSONObject, TJSONArray), можете запитувати значення, ітерувати, серіалізувати. Це надійно для типових інтеграцій у бізнес-середовищі, але має дві наслідки:

  • Це не справжній стрімінговий парсер: об’єктна модель будується повністю. Можливо, ви заощадите крок зчитування в окремий рядок, але DOM залишиться пам’ятемістким.
  • Вхідні дані парсера зазвичай — текст: залежно від версії Delphi і використовуваного API ви швидко повертаєтеся до роботи зі рядком, включно з перетворенням кодування.

Якщо ваша мета — «швидкий стрім-парсер», на практиці це зазвичай означає одне з двох: (1) відсутність непотрібних копій і (2) якнайраніше fail-fast при пошкоджених Payloads. Обидва підходи досяжні зі System.JSON, якщо ви контролюєте етап перетворення байтів у текст.

Підводні камені UTF-8 зі спеціальними символами: типові причини

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking ist harmlos – solange der UTF-8-Decoder Mehrbyte-Sequenzen über Grenzen puffert.

Wenn Umlaute (ä/ö/ü/ß) oder andere Sonderzeichen im Ergebnis falsch aussehen (ä, – etc.), ist das fast immer ein Encoding-Mismatch. Im Delphi-Umfeld sind diese Ursachen besonders häufig:

1) ANSI-Fallback durch „bequeme“ Helper

Einige Lesewege nehmen stillschweigend das System-ANSI-Encoding (Codepage des Windows-Systems) an, wenn keine explizite Kodierung übergeben wird. Das fällt erst auf, wenn ein Payload nicht nur ASCII enthält. In Testdaten ist das oft zufällig „okay“, in Produktion knallt es bei echten Namen, Orten, Freitexten.

2) BOM-Verwirrung (Byte Order Mark)

UTF-8 kann mit BOM beginnen (Bytes EF BB BF). Im Web-Kontext ist BOM eher unüblich, bei Dateien kommt es vor. Manche Reader erkennen BOM und passen das Encoding an, andere nicht oder nur in bestimmten Modi. Wenn ein BOM als normales Zeichen in den String rutscht, siehst du oft ein unsichtbares „Zero Width No-Break Space“ am Anfang oder der JSON-Parser scheitert direkt am ersten Token.

3) Doppel-Konvertierung (UTF-8 wird „nochmal“ interpretiert)

Das klassische Fehlerbild „ä“ statt „ä“ entsteht, wenn UTF-8-Bytes zuerst korrekt nach Unicode decodiert wurden, aber später nochmals als ANSI/UTF-8-Bytes fehlinterpretiert werden (oder umgekehrt). In Delphi passiert das gern, wenn zwischen TBytes, RawByteString und string unklar konvertiert wird.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF-8 kodiert Sonderzeichen in 2–4 Bytes. Wenn du in Chunks liest (z. B. 8 KB) und die Chunk-Grenze mitten im Zeichen liegt, muss der Decoder das sauber puffern. Ein naiver Ansatz, der jeden Chunk separat in String umwandelt und zusammenklebt, produziert ungültige Sequenzen. Das kann als „sporadischer“ Fehler wirken, abhängig von Paketgrenzen, Proxy-Verhalten oder HTTP-Chunking.

5) Falsche Annahmen aus HTTP-Headern

Bei REST ist die Quelle oft Content-Type: application/json; charset=utf-8. Manche Server liefern aber kein charset, manche liefern falsche Angaben. Wenn du blind den Header nutzt, kann das je nach Backend-Version wechseln. Für Betrieb und Support ist es hilfreich, den tatsächlichen Byte-Stream zu prüfen und im Fehlerfall zu loggen.

Ein sauberer Ansatz: Stream → UTF-8-Decoder → JSON-Parser

Die robuste Pipeline besteht aus drei klaren Stufen:

  1. Bytes aus dem Stream lesen (kontrolliert, ggf. mit Limit/Timeout im HTTP-Client).
  2. Decoding nach Unicode mit explizitem UTF-8 (BOM optional tolerieren).
  3. Parsing mit System.JSON in ein Objektmodell oder in gezielte Extraktion.

Der wichtigste Hebel ist Stufe 2: Du willst nicht, dass irgendwo „Default Encoding“ entscheidet. In Delphi bedeutet das: TEncoding.UTF8 explizit setzen und nicht auf implizite Konvertierungen vertrauen.

Was „schnell“ hier konkret bedeutet

За допомогою System.JSON ти не «оптимізуєш» DOM. Але можна уникнути:

  • додаткової копії всієї Payload як проміжного рядка, якщо всередині тобі потрібні лише кілька значень (у такому випадку більше підходить інший парсер; про це пізніше),
  • багатократного перекодування,
  • та ти можеш контрольовано читати дуже великі Payload (з обмеженням розміру та чітким повідомленням про помилку), замість того щоб завершуватися Out-of-Memory або Access Violations.

Конкретний крайній випадок: спецсимволи пошкоджуються, але лише іноді

Один приклад із практики особливо підступний: payload по суті є коректним UTF-8-JSON, але ти читаєш його чанками (Chunks) і конвертуєш кожен чанк у String. Поки зустрічається лише ASCII, ти нічого не помічаєш. Як тільки символ з умлаутом потрапляє точно на межу чанка, виникають некоректні UTF-8-послідовності. Наслідок: або пошкоджені символи, або помилка парсера в місці, що не відповідає реальному вмісту.

Як це розпізнати:

  • Parse-помилки виникають «випадково» при великих відповідях, а не при невеликих.
  • Той самий Request іноді проходить, іноді ні (залежно від Chunking/Transport).
  • Hexdump байтів показує коректне UTF-8, але твій задокументований String містить Replacement Characters (�) або класичне Mojibake.

Рішення не в тому, щоб «mehr zu readln» або «größere Buffer» використовувати, а в тому, щоб застосувати декодер, який правильно буферизує багатобайтові послідовності через межі чанків. Саме тут TStreamReader у поєднанні з UTF-8-Encoding може бути корисний — якщо ти його правильно ініціалізуєш.

Практичний посібник: відтворювана перевірка UTF-8 у Delphi

Debugging-Setup mit Byte-Dump-Ausdrucken und Arbeitsmaterial, um UTF-8-Bytes und BOM in JSON-Payloads zu prüfen
Для коректного налагодження перше, на що треба звернути увагу — це байтовий потік: BOM, усічення та некоректні послідовності стають видимими швидко.

Перш ніж чіпати парсер, потрібне налагоджувальне середовище, яке робить видимою фактичну послідовність байтів. Для служби підтримки та експлуатації це на вагу золота, бо пізніше ти зможеш чітко сказати, чи відправник дає неправильні дані, чи твій конвеєр неправильно декодує.

1) Перевірити перші байти (BOM, початок JSON)

Якщо JSON приходить з BOM, на початку потоку ти побачиш EF BB BF. Безпосередньо після цього зазвичай має йти «{» або «[». Якщо в рядку вже з’являється «ï»»¿» (), BOM було не розпізнано як BOM, а декодовано як текст.

2) Логування сирих байтів у Hex — але обмежено

Не логуй повні payload у продакшні (з міркувань захисту даних, витрат, обсягу логів). Практично себе виправдали:

  • префікс (наприклад перші 256 або 1024 байти),
  • суфікс (останні 256 байт),
  • та хеш (SHA-256) для кореляції, якщо потрібно порівнювати payload.

Це дозволяє в багатьох випадках розібратися з проблемами спецсимволів за лічені хвилини: чи правильна байтова послідовність для «ä» (C3 A4)? Чи має місце усічення? Чи з’являється несподіване 0x00 (null-байт), наприклад через хибну інтерпретацію як UTF-16?

3) Логувати Content-Type і Charset

При HTTP/REST: фіксуйте Content-Type і вказаний charset. Якщо байти однозначно UTF-8, але charset заявляє інше, клієнт не має діяти сліпо за цим. Для JSON UTF-8 є де-факто стандартом. У сумнівах: аналіз байтів переважає заголовок.

Швидкий парсер потоку з System.JSON: дизайн без зайвих копій

Schemahafte Darstellung einer Pipeline aus Stream, UTF-8-Decoding, Größenlimit und JSON-DOM-Parsing in Delphi
Чітка конвеєрна схема з лімітом і явним UTF-8 відокремлює транспорт, декодування і парсинг чисто.

Практичний патерн: читаєте зі стріму у байтовий буфер, створюєте з нього рядок рівно один раз з кодуванням UTF-8 і передаєте цей рядок JSON-парсеру. Це не «стрімінг» у сенсі SAX, але це контрольована, продуктивна конвеєрна схема без несподіваних змін кодування.

Важливе для архітектури: реалізуйте функцію так, щоб вона централізовано приймала рішення в одному місці:

  • Які правила кодування застосовуються (зазвичай UTF-8, BOM — опціонально)?
  • Який максимальний розмір корисного навантаження (захист від DoS, операційні обмеження)?
  • Як виглядають повідомлення про помилки (з контекстом, але без витоку даних)?

Стратегія читання: обмежене буферизування замість „StreamToString“ без ліміту

Якщо приймаєте JSON із зовнішніх джерел (партнери, мобільні клієнти, сторонні постачальники), обов’язковий ліміт розміру. Без ліміту один нещасливий запит може довести сервіс до Memory Pressure. Практично це означає: під час читання підраховувати суму байтів і при досягненні межі припиняти — з чітким винятком, зрозумілим у моніторингу.

Чому я часто очікую UTF-8 «без BOM», але BOM буду толерувати

У REST-payloadах BOM зустрічається рідко. Натомість у файлах (експорт, ручне редагування) — частіше. Для стійких імпортних потоків доцільно толерувати BOM, але фіксувати його в логах, оскільки це може бути індикатором «світу файлів» замість «світу API».

Тихі вбивці: налаштування TStreamReader за замовчуванням та змішане використання текстових рідерів

TStreamReader зручний, але потрібно чітко контролювати дві речі:

  • Чітко задавайте кодування: не розраховуйте на те, що він його «розпізнає».
  • Розуміти буферизацію: Reader виконує внутрішнє буферизування. Якщо потім читати той самий стрім в іншому місці, позиція має значення. Це звучить тривіально, але в більших імпортних конвеєрах швидко стає джерелом помилок.

Особливо неприємний змішаний режим: спочатку читаєш частину як байти (наприклад для логування або перевірки магічних байтів), потім продовжуєш з TStreamReader. Якщо при цьому не відкотити позицію або не ініціалізувати рідер на правильній позиції, читатиметься з байта 257 замість 0. JSON-парсер тоді викине „Invalid character at position …“, хоча сама payload коректна.

Якщо спецсимволи попри UTF-8 «ламані»: Escaping vs. справжній Unicode

JSON може містити спеціальні символи двома способами:

  • Як справжні символи UTF-8 (наприклад „München“ як байти C3 BC …).
  • Як escape-послідовність (наприклад, „Mu00fcnchen“).

Обидва варіанти дійсні. У практиці важливо: escape-послідовності обходять багато проблем транспортування, але вони лише начебто приховують помилки кодування. Якщо ваша система деінде неправильно інтерпретує байти, це ризик для експлуатації, а не просто косметичний баг. Крім того, escape-послідовності можуть дезорієнтувати при логуванні/моніторингу, якщо команди очікують бачити „читабельний текст“.

System.JSON в обох випадках в кінці надає тобі звичайні Delphi-строки (UTF-16), за умови, що шлях до цього був коректним.

Реалістична оцінка продуктивності: витрати DOM, великі масиви та селективність

Найбільший важіль підвищення продуктивності часто не в тому, щоб «зробити парсер швидшим», а в тому, щоб парсити менше. З System.JSON це складно, оскільки ти отримуєш DOM. Три типові ситуації:

  • Великі масиви (10.000+ елементів): Побудова DOM забирає час і пам’ять. Якщо тобі потрібно лише 2 поля на елемент, часто доцільніший парсер з підтримкою стрімінгу (SAX/Tokenizer). System.JSON для цього не призначений.
  • Окремі об’єкти з багатьма полями: Якщо тобі потрібно лише кілька полів, все ще можна використовувати DOM, але уникай багаторазового обходу. Отримуй значення один раз і відмаплюй їх у свої структури.
  • Кілька великих payload-ів підряд: У імпортних задачах або Sync-Worker варто інкапсулювати парсинг у чіткий крок і після кожного документа звільняти всі посилання, щоби Memory-Manager міг прибрати пам’ять. Це банально, але в сервісах часто роблять «побічно».

Отже, «швидкий stream-парсер» з System.JSON часто є хорошим компромісом: читання ефективно і коректно, DOM використовувати свідомо, визначати межі. Якщо тобі потрібна справжня семантика стрімінгу (наприклад, обробка елементів масиву по черзі без утримування всього у пам’яті), System.JSON не підходить як база.

Стійкість у експлуатації: шаблони помилок і як зробити їх відразу діагностичними

Помилки парсингу JSON у логах часто не інформативні, бо вони вказують лише позицію. Для експлуатації та підтримки потрібен контекст:

  • Байтова позиція vs. позиція символу: Для UTF-8 це не тотожне. Якщо парсер повідомляє позицію символу, байтова позиція може відрізнятися. Для байтових дампів вирішальна байтова позиція.
  • Фрагмент навколо місця помилки: Логуй у випадку помилки невелике вікно навколо позиції (наприклад, 40 символів до/після), але лише якщо там немає чутливих даних. Альтернативно: логувати тільки у шістнадцятковому вигляді.
  • Кореляція: Request-ID, Endpoint, Partner-ID, Payload-Hash. Інакше ти ніколи не знайдеш „той самий“ баг знову.

Мета — щоб після інциденту в продакшні ти протягом кількох хвилин міг відповісти: „кодування інтерпретовано неправильно“, „payload обрізано“, „сервер повернув invalid JSON“ або „у нас проблема з мапінгом“.

Уникнення підводних каменів UTF-8: чекліст

  • Кодування завжди явно вказувати: При читанні зі стріму та при записі в логи/файли не довіряти налаштуванням за замовчуванням.
  • Ніякого конвертування chunk->string: Якщо читаєш шматками (chunked), збирай байти або використовуй декодер, який буферизує мультибайтові послідовності.
  • Толерантність до BOM, але його видно: Приймати, але в дебагу мати можливість його виявити.
  • Встановлювати ліміти: макс. розмір payload, макс. глибина об’єктів/масивів (якщо ти це контролюєш), таймаути в HTTP-клієнті.
  • Розділення відповідальностей: ізолюйте «читання транспорту» і «парсинг JSON» окремо. Так відлагодження проходить швидше і пізніше можна буде замінити парсер.
  • Коли зусилля для потокового парсера справді виправдані?

    Вам не потрібно оптимізувати кожне місце з JSON. Підхід зазвичай має сенс, якщо щонайменше одне з наступного справджується:

    • Великі payload’и (кілька МБ) зустрічаються регулярно або можуть виникати.
    • Довготривалі процеси (сервіс, worker) обробляють багато payload’ів і ви спостерігаєте стрибки пам’яті або фрагментацію.
    • Взаємодія з гетерогенними системами: кілька партнерів, різні платформи, іноді некоректні кодування.
    • Історія інцидентів: вже були «зіпсовані умлаути», періодичні помилки парсинга або важко відтворювані переривання імпорту.

    Якщо ваші payload’и малі і походять з контрольованого джерела, часто достатньо простого підходу — але навіть тоді: явно встановити UTF-8 майже нічого не коштує і запобігає пізнішим сюрпризам.

    Відмежування: коли потрібне справжнє потокове оброблення

    System.JSON орієнтований на DOM. Якщо ви хочете обробляти дані справді «в потоці», напр., велике масив елемент за елементом, не утримуючи його повністю, потрібен інший підхід до парсингу (Tokenizer/SAX). Це не оцінка, а архітектурне рішення:

    • DOM (System.JSON): зручно, підходить для типових бізнес-об’єктів, але витратний щодо пам’яті.
    • Streaming/SAX: менше споживання пам’яті, підходить для дуже великих даних, але вимагає більше роботи при реалізації та ретельнішої обробки помилок.

    Розумний компроміс часто такий: правильно реалізувати оброблення потоків, кодування та ліміти, а потім вирішувати, чи підходить DOM. У багатьох проєктах навіть це вже значно стабілізує експлуатацію.

    Висновок: JSON in Delphi стане надійним, якщо ви розглядатимете кодування та потоки як окремий шар

    Більшість проблем навколо «JSON in Delphi» не пов’язані з самим JSON-парсером, а з непомітним фрагментом перед ним: байти зі стріму перетворюються на текст. Якщо ви там явно обробляєте UTF-8, розпізнаєте випадки BOM, не ігноруєте межі чанків і встановлюєте чіткі обмеження розміру, типові помилки зі спеціальними символами зникають — і спорадичні помилки парсингу стають відтворюваними.

    System.JSON при цьому залишається прагматичним стандартом: не найшвидший стрімінговий парсер, але надійний, якщо ви контролюєте вхідні дані і свідомо приймаєте витрати DOM. Якщо хочете, ми можемо разом пройти ваш конкретний шлях імпорту/REST і виявити місце, де підводить кодування або чанкінг: зв’яжіться з нами.

    Для цієї теми також важливі потокові JSON-парсери. Стаття розкладає ці аспекти зрозуміло і показує, на що звертати увагу в повсякденні.

    Обговорити проєкт або модернізацію з Net-Base.

    Наступний крок

    Якщо тема перетворюється на реальний проєкт, архітектуру, наявні системи та експлуатацію слід розглядати разом на ранньому етапі.

    Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

    • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
    • REST, доступ до даних, портали та Rollout не відсуваються на пізніший етап.
    • Ви заздалегідь бачите, який шлях є економічно та операційно життєздатним.

    Поділитися дописом

    Поділитися цим дописом безпосередньо

    LinkedIn, X, XING, Facebook, WhatsApp та E‑Mail доступні негайно. Для Instagram ми безпосередньо готуємо посилання та короткий текст.

    Електронна пошта

    Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.