Net-Base Списание

13.08.2026

JSON в Delphi: Бърз потоков парсър с System.JSON + подводни камъни при UTF-8 и специални знаци

Когато JSON-Payloads в Delphi идват директно от поток, от „просто да парснеш“ бързо се получава проблем в продукция: висока консумация на памет, спорадични грешки при парсване и неправилно изобразяване на умлаутите. Този практичен материал показва как да изградиш бърз Stream-парсър с System.JSON...

13.08.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

„JSON in Delphi“ звучи като решен проблем: System.JSON е на борда, REST-извикванията връщат текст, край. На практика обаче истинските грешки възникват там, където JSON не е наличен като удобен String, а като Stream: HTTP-Response-Stream, Datei-Stream, Named Pipe, Message-Queue или голям BLOB от базата данни. Тогава се събират три фактора, които в практиката често се подценяват: поведение на паметта, кодиране на символите (особено UTF-8) и гранични случаи, свързани със специални знаци.

Този материал показва един чист, бърз подход за парсване на JSON от TStream без да се създават излишни копия — и преди всичко без типичните UTF-8 капани, при които умлаутите се повреждат или парсерите спорадично прекъсват с криптични съобщения. Фокусът е върху въздействието върху експлоатацията и стабилността на интерфейсите: възпроизводимо дебъгване, ясни граници на подхода и критерии кога усилието наистина си заслужава.

Защо Streams при JSON-парсване в Delphi работят по различен начин

Докато JSON-документът е малък, пътят „четене на Stream в String, после parse“ е удобен. Но при определен обем на payload-а (типично: големи списъци, отчети, синхронизиращи данни, износ на логове) това става скъпо:

  • Дублиране на паметта: Четеш байтове в буфер, преобразуваш в Unicode-String (Delphi-String = UTF-16), парсерът вътрешно създава допълнителни структури. Това може кратковременно да доведе до няколко копия.
  • Натиск върху GC/Heap: Много временни низове и 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), можеш да извличаш стойности, да итерираш, да сериализираш. Това е устойчиво за типични бизнес интеграции, но има две последствия:

  • Това не е истински стрийминг-парсер: Обектният модел се изгражда изцяло. Може да спестиш евентуално четенето в отделен String, но DOM-ът остава паметно интензивен.
  • Входът за парсера обикновено е текст: В зависимост от версията на Delphi и използваната API бързо отново се стига до String, включително конвертиране на енкодинга.

Ако целта ти е „бърз Stream-парсер“, на практика обикновено имаш предвид едно от две неща: (1) никакви излишни копия и (2) възможно най-ранно Fail-fast при дефектни payload-и. И двете могат да се постигнат с 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, но ти го четеш на чанкове и конвертираш всеки чанк в низ. Докато има само ASCII, не забелязваш нищо. Щом обаче умлаутът попадне точно на границата на чанк, се получават невалидни UTF-8 последователности. Резултат: или повредени символи, или грешка при парсване на място, което не съответства на действителното съдържание.

Как да го разпознаеш:

  • Грешки при парсване възникват „случайно“ при големи отговори, не при малки.
  • Същата Request понякога минава, понякога не (в зависимост от Chunking/Transport).
  • Hexdump на байтовете показва валидно UTF-8, но логнатият ти низ съдържа Replacement Characters (�) или класическо Mojibake.

Решението не е да „четеш повече“ или да използваш „по-големи буфери“, а да използваш декодер, който правилно буферира многобайтовите последователности през границите на чанковете. Това е точно моментът, в който 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, прекъсвания и невалидни последователности се виждат бързо.

Преди да променяш парсъра, ти трябва дебъгинг-setup, което прави видима реалната последователност от байтове. За поддръжка и експлоатация това е безценно, защото по-късно можеш ясно да кажеш дали срещуположната страна доставя грешни данни или твоята верига неправилно декодира.

1) Die ersten Bytes prüfen (BOM, JSON-Start)

Ако JSON идва с BOM, в началото на потока виждаш EF BB BF. Непосредствено след това обикновено трябва да следва „{“ или „[“. Ако в низа вече стои „ï»¿“, BOM не е бил третиран като BOM, а е декодиран като текст.

2) Rohbytes in Hex loggen – aber begrenzt

Не логвайте пълните Payload-и в продукция (Datenschutz, Kosten, Log-Volumen). Добре работят:

  • префикс (напр. първите 256 или 1024 байта),
  • суфикс (последните 256 байта),
  • и хеш (SHA-256) за корелация, ако трябва да сравняваш Payload-и.

Така често можеш да класифицираш проблеми със специални знаци за минути: правилна ли е байтовата последователност за „ä“ (C3 A4)? Има ли обрязване на потока? Появява ли се неочаквано 0x00 (нулев байт), например поради грешна предпоставка за UTF-16?

3) Content-Type und Charset mitloggen

При HTTP/REST: логирай Content-Type и декларирания charset. Ако байтовете са явно UTF-8, но charset твърди друго, не следвай това сляпо в клиента. За JSON UTF-8 е де-факто стандарт. При съмнение: анализът на байтовете побеждава header-а.

Бърз Stream-парсър с System.JSON: дизайн без ненужни копия

Схематично представяне на пайплайн от Stream, UTF-8-декодиране, лимит на размера и JSON-DOM-парсинг в Delphi
Ясна пайплайн с лимит и експлицитен UTF-8 разделя чисто транспорта, декодирането и парсинга.

Практически приложимият патърн е: четеш от Stream в байтов буфер, изграждаш от него стринг точно веднъж с UTF-8 и предаваш този стринг на JSON-парсъра. Това не е „Streaming“ в смисъла на SAX, но е контролирана, високопроизводителна пайплайн без изненадващи смени на енкодинга.

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

  • Кое правило за енкодиране важи (обикновено UTF-8, BOM по избор)?
  • Колко голяма може да бъде максимално payload-ът (защита срещу DoS, оперативен лимит)?
  • Как изглеждат съобщенията за грешки (с контекст, но без изтичане на данни)?

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

Ако приемаш JSON от външни източници (партньори, мобилни клиенти, трети страни), лимит за размер е задължителен. Без лимит една нещастна заявка е достатъчна да подложи сервиса на натиск върху паметта. Практически това означава: при четене брои общия брой байтове и прекъсни при достигане на граница – с ясно изключение, което е разбираемо в мониторинга.

Защо често очаквам UTF-8 без BOM, но бих толерирал BOM

При REST-payloads BOM се явява рядко. При файлове (експорти, ръчна редакция) — по-често. За надеждни импортни потоци е разумно BOM да се толерира, но да се логва, защото може да е индикация за „файлов свят“ вместо „API-свят“.

Тихите убийци: стойности по подразбиране на TStreamReader и смесен режим с текстови рийдъри

TStreamReader е удобен, но трябва да имаш два неща под контрол:

  • Задавай енкодинга експлицитно: Не се надявай, че той ще „разпознае“.
  • Разбери буферирането: Рийдърът буферира вътрешно. Ако по-късно четеш същия Stream на друго място, позицията е релевантна. Това звучи тривиално, но в по-големи импортни потоци бързо се превръща в източник на грешки.

Особено неприятен е смесеният режим: първо прочетеш част като байтове (например за логване или magic-байтове), после продължиш с TStreamReader. Ако не върнеш чисто позицията или не инициализираш рийдъра на правилната позиция, ще четеш от байт 257 вместо от 0. JSON-парсърът ще докладва „Invalid character at position …“, въпреки че самата payload е коректна.

Когато специалните знаци въпреки UTF-8 остават „чупени“: Escape-последователности 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 струва време и RAM. Ако ти трябват само 2 полета на елемент, парсър с поддръжка на стрийминг (SAX/Tokenizer) често е по-подходящ. System.JSON не е направен за това.
  • Единични обекти с много полета: Ако ти трябват само няколко полета, все пак можеш да използваш DOM, но избягвай многократно обхождане. Извличай стойностите веднъж и ги мапирай в структурите си.
  • Няколко големи Payloads последователно: При Import-Jobs или Sync-Worker-и има смисъл да капсулираш парсирането в отделна стъпка и след всеки документ да освобождаваш всички референции, за да може мениджърът на паметта да почисти. Това е банално, но в Services често се прави „между другото“.

Затова „бърз Stream-Parser“ с System.JSON често е добър компромис: четене ефективно и коректно, съзнателно използване на DOM, дефиниране на граници. Ако имаш нужда от истинска streaming семантика (напр. обработка на елементи на масив последователно, без да задържаш всичко), System.JSON не е подходяща база.

Робустност в експлоатация: модели на грешки и как да ги направиш веднага проследими

Грешките при парсване на JSON често не са полезни в логовете, защото посочват само позиция. За експлоатация и поддръжка ти трябва контекст:

  • Позиция в байтове срещу позиция в знаци: При UTF-8 това не е едно и също. Ако парсърът съобщава позиция по знаци, байтовата позиция може да се различава. За байтови дампове байтовата позиция е решаваща.
  • Откъс около мястото на грешката: Логвай при грешка малък прозорец около позицията (напр. 40 знака преди/след), но само ако не съдържа чувствителни данни. Алтернативно: логвай само в шестнадесетичен вид.
  • Корелация: Request-ID, Endpoint, Partner-ID, Payload-Hash. Иначе никога няма да намериш „ден един грешка“ отново.

Целта е след продуктивен инцидент да можеш в рамките на няколко минути да отговориш: „Encoding falsch interpretiert“, „Payload abgeschnitten“, „Server liefert invalid JSON“ или „wir haben ein Mapping-Problem“.

UTF-8-Fallstricke gezielt vermeiden: Checkliste

  • Кодирането винаги експлицитно: При четене от поток и при запис в логове/файлове не се доверявай на подразбиращите се настройки.
  • Никакво Chunk-zu-String-Konvertierung: Ако четеш на чанкове, събирай байтовете или използвай декодер, който буферира многобайтовите последователности.
  • BOM толерантен, но видим: Приемай го, но трябва да можеш да го разпознаеш при дебъгване.
  • Задай лимити: макс. Payload-Größe, макс. Objekt-/Array-Tiefe (ако можеш да го контролираш), Timeouts im HTTP-Client.
  • Разделяне на отговорностите: „четене на транспорта“ и „парсиране на JSON“ да се капсулират отделно. Така отстраняваш грешки по-бързо и по-късно можеш да замениш парсера.
  • Кога наистина си заслужава усилието за поточен парсер?

    Не е необходимо да оптимизираш всеки JSON фрагмент. Подходът обикновено си струва, когато поне едно от следните е вярно:

    • Големи payloads (няколко MB) се появяват редовно или биха могли да възникнат.
    • Longrunning-Prozesse (Service, Worker) обработват много payloads и наблюдаваш паметови пикове или фрагментация.
    • Интероперабилност с хетерогенни системи: няколко партньора, различни платформи, от време на време неправилни кодировки.
    • Incident-Historie: Вече е имало „повредени умлаути“, спорадични грешки при парсване или трудно възпроизвеждащи се прекъсвания на импорта.

    Ако твоите payloads са малки и идват от контролиран източник, често е достатъчен прост подход – но дори и тогава: изрично задаване на UTF-8 струва почти нищо и предотвратява по-късни изненади.

    Разграничение: Кога наистина имаш нужда от истинско стриймване

    System.JSON е ориентиран към DOM. Ако искаш да обработваш данни наистина „в движение“, например голям масив елемент по елемент, без да го държиш изцяло в паметта, ти се нуждаеш от друг подход за парсване (Tokenizer/SAX). Това не е оценъчно твърдение, а архитектурно решение:

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

    Често разумен компромис е: да изградиш чисто Stream-Handling, третирането на Encoding и лимитите, и едва след това да решиш дали DOM все още е подходящ. В много проекти само това значително стабилизира експлоатацията.

    Заключение: JSON в Delphi работи надеждно, ако третираш Encoding и Streams като отделен слой

    Повечето проблеми около „JSON в Delphi“ не произтичат от самия JSON-парсер, а от незабележимия участък преди него: байтове от поток се превръщат в текст. Ако там третираш UTF-8 експлицитно, разпознаваш случаи с BOM, не игнорираш границите на чънковете и задаваш ясни размерни лимити, типичните грешки със специални знаци изчезват – и спорадичните грешки при парсване стават възпроизводими.

    System.JSON остава прагматичен стандарт: не е най-бързият стрийминг-парсер, но е солиден, ако контролираш входа и съзнателно приемаш разходите за DOM. Ако искаш, можем заедно да прегледаме твоя конкретен Import-/REST-път и да идентифицираме мястото, където Encoding или Chunking се провалят: Свържи се с нас.

    За тази тема също са важни JSON Stream Parser. Статията подрежда тези аспекти по разбираем начин и показва на какво се обръща внимание в ежедневната работа.

    Обсъди проект или модернизационно начинание с Net-Base.

    Следваща стъпка

    Когато темата прерасне в реален проект, архитектурата, съществуващите активи и експлоатацията трябва да се разглеждат заедно още в ранния етап.

    Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

    • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
    • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последващи задачи.
    • Вие виждате навреме кой път е икономически и оперативно жизнеспособен.

    Сподели публикацията

    Споделете тази публикация директно

    LinkedIn, X, XING, Facebook, WhatsApp и електронна поща са налични веднага. За Instagram подготвяме директно връзка и кратък текст.

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

    Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.