Net-Base Magazín

13.08.2026

JSON v Delphi: Rychlý streamový parser se System.JSON + úskalí UTF-8 u speciálních znaků

Když JSON-Payloads v Delphi přicházejí přímo ze streamu, z 'rychlého parsování' se snadno stane produkční problém: vysoké nároky na paměť, sporadické chyby parsování a poškozené diakritické znaky. Tento praktický článek ukazuje, jak pomocí System.JSON vytvoříš rychlý stream-parser...

13.08.2026

Od tématu magazínu k projektové praxi

Vhodné stránky služeb a technické stránky k příspěvku

„JSON v Delphi“ zní jako vyřešený problém: System.JSON je na palubě, REST-volání vracejí text, hotovo. V praxi se ale skutečné chyby objevují tam, kde JSON není k dispozici jako pohodlný řetězec, ale jako stream: HTTP response stream, souborový stream, pojmenovaná pipe, message queue nebo velký BLOB z databáze. Pak se sejde tři věci, které se v každodenním provozu často podceňují: chování paměti, kódování znaků (zejména UTF-8) a okrajové případy kolem speciálních znaků.

Tento článek ukazuje čistý, rychlý přístup k parsování JSON z TStream bez zbytečného vytváření kopií – a především bez typických pastí UTF-8, při kterých se diakritické znaky „poškozují“ nebo parsery příležitostně skončí s kryptickými chybami. Důraz je kladen na dopady na provoz a stabilitu rozhraní: reprodukovatelné debugování, jasné hranice přístupu a kritéria, kdy se námaha skutečně vyplatí.

Proč streamy při JSON-parsování v Delphi fungují jinak

Dokud je JSON dokument malý, je cesta „přečíst stream do stringu, pak parse“ pohodlná. Po dosažení určité velikosti payloadu (typicky: velké seznamy, reporty, synchronizační data, exporty logů) se to ale stává nákladným:

  • Duplikace v paměti: Čteš bajty do bufferu, převedeš je na Unicode-String (Delphi-String = UTF-16), parser interně vytváří další struktury. To může krátkodobě znamenat několik kopií.
  • Zátěž GC/heapu: Mnoho dočasných stringů a JSON-hodnot zvyšuje fragmentaci a režii alokací, zvlášť v dlouho běžících procesech (služby, workery, importní úlohy).
  • Chybný symptom: Pokud se při načítání udělají nesprávné předpoklady o kódování, parser uvidí jen „divné znaky“ nebo neočekávané řídicí bajty.

Důležité je jasné oddělení: JSON je formálně Unicode, v přenosu je to téměř vždy UTF-8. Delphi ale interně pracuje s UTF-16. Přechod z bajtů (stream) na znaky (string) je místo, kde vznikají problémy se speciálními znaky – ne v samotném JSONu.

System.JSON: Co umí dobře – a kde musíš být opatrný

System.JSON je v Delphi standard pro DOM‑based JSON: dostaneš objektový model (TJSONObject, TJSONArray), můžeš dotazovat hodnoty, iterovat, serializovat. To je robustní pro typické business integrace, ale má to dvě důsledky:

  • Není to skutečný streaming parser: Objektový model se vybuduje celý. Sice můžeš ušetřit případné načtení do extra stringu, ale DOM zůstává náročný na paměť.
  • Vstup parseru je zpravidla text: Podle verze Delphi a použitého API se rychle zase vrátíš k práci se stringem včetně konverze kódování.

Pokud je tvým cílem „rychlý stream‑parser“, v praxi to obvykle znamená jedno ze dvou: (1) žádné zbytečné kopie a (2) co nejranější fail‑fast při poškozených payloadech. Obě věci jsou s System.JSON dosažitelné, pokud máš pod kontrolou krok z bajtů na text.

UTF-8 pasti u speciálních znaků: typické příčiny

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking je neškodný – pokud UTF‑8 dekodér vyrovnává vícebajtové sekvence přes hranice.

Pokud přehlásky (ä/ö/ü/ß) nebo jiné speciální znaky ve výsledku vypadají nesprávně (ä, – atd.), jde téměř vždy o nesoulad kódování. V prostředí Delphi jsou tyto příčiny obzvlášť časté:

1) ANSI-Fallback durch „bequeme“ Helper

Některé cesty čtení implicitně předpokládají systémové ANSI kódování (Codepage des Windows-Systems), pokud není předáno explicitní kódování. To se projeví až tehdy, když payload neobsahuje jen ASCII. V testovacích datech to často náhodně „funguje“, v produkci to narazí u skutečných jmen, míst a volného textu.

2) BOM-Verwirrung (Byte Order Mark)

UTF‑8 může začínat BOM (bajty EF BB BF). V kontextu webu je BOM spíš neobvyklý, u souborů se vyskytuje. Některé čtečky BOM rozpoznají a přizpůsobí kódování, jiné ne, nebo jen v určitých režimech. Pokud se BOM dostane do řetězce jako obyčejný znak, často uvidíš na začátku neviditelný „Zero Width No‑Break Space“ nebo JSON‑parser selže hned na prvním tokenu.

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

Klasický symptom „ä“ místo „ä“ vzniká, když jsou UTF‑8 bajty nejprve správně dekódovány na Unicode, ale později jsou znovu chybně interpretovány jako ANSI/UTF‑8 bajty (nebo naopak). Ve prostředí Delphi se to často stává, když dochází k nejasným konverzím mezi TBytes, RawByteString a string.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF‑8 kóduje speciální znaky do 2–4 bajtů. Pokud čteš po chunkech (např. 8 KB) a hranice chunku leží uprostřed znaku, musí dekodér tyto bajty korektně pufrovat. Naivní přístup, který každý chunk zvlášť převádí na řetězec a pak je slepí dohromady, vytvoří neplatné sekvence. To může působit jako „sporadická“ chyba, závislá na hranicích paketů, chování proxy nebo HTTP chunkingu.

5) Falsche Annahmen aus HTTP-Headern

U REST je zdroj často Content-Type: application/json; charset=utf-8. Některé servery ale neposílají charset, jiné posílají nesprávné údaje. Pokud slepě spoléháš na hlavičku, může se to měnit s verzí backendu. Pro provoz a podporu je užitečné zkontrolovat skutečný bytový tok a v případě chyby jej zapisovat do logu.

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

Robustní pipeline se skládá ze tří jasně oddělených fází:

  1. Číst bajty ze streamu (kontrolovaně, případně s limitem/timeoutem v HTTP klientu).
  2. Dekódování na Unicode s explicitním UTF‑8 (BOM lze volitelně tolerovat).
  3. Parsování pomocí System.JSON do objektového modelu nebo cílené extrakce.

Nejdůležitější páka je fáze 2: nechceš, aby někde rozhodovalo „Default Encoding“. Ve prostředí Delphi to znamená: explicitně nastavit TEncoding.UTF8 a nespoléhat se na implicitní konverze.

Co „schnell“ zde konkrétně znamená

S System.JSON DOM „nevyoptimalizuješ“. Ale můžeš se vyhnout:

  • dodatečné kopii celé payload jako mezistrunu, pokud interně stejně potřebuješ jen několik hodnot (v tom případě se spíš vyplatí jiný parser; k tomu později),
  • vícenásobnému překódování,
  • a můžeš velmi velké payloady kontrolovaně načítat (s omezením velikosti a jasnou chybovou hláškou), místo aby to skončilo Out-of-Memory nebo Access Violations.

Konkrétní okrajový případ: speciální znaky poškozené, ale jen občas

Okrajový případ z praxe je obzvlášť zákeřný: payload je ve své podstatě platné UTF-8-JSON, ale čteš jej v chunkech a převádíš každý chunk na řetězec. Dokud se vyskytuje pouze ASCII, nic nepoznáš. Jakmile se právě diakritické písmeno (Umlaut) nachází přesně na hranici chunku, vznikají neplatné UTF-8 sekvence. Výsledek: buď poškozené znaky, nebo chyba parsování na místě, které s obsahem nesouvisí.

Jak to poznáš:

  • Chyby parsování se objevují „náhodně“ u velkých odpovědí, ne u malých.
  • Tentýž request občas projde, občas ne (podle chunkingu/transportu).
  • Hexdump bytů ukáže platné UTF-8, ale ve zalogovaném řetězci jsou náhradní znaky (�) nebo klasické mojibake.

Řešením není „číst víc“ nebo „větší buffery“, ale použít dekodér, který vícestavové (multibyte) sekvence přes hranice chunků korektně pufruje. To je přesně okamžik, kdy může být TStreamReader v kombinaci s UTF-8 kódováním užitečný – pokud jej správně inicializuješ.

Praktický návod: opakovaně ověřit UTF-8 v Delphi

Debugging-Setup mit Byte-Dump-Ausdrucken und Arbeitsmaterial, um UTF-8-Bytes und BOM in JSON-Payloads zu prüfen
Pro čisté debugování je prvořadý byte-stream: BOM, truncace a neplatné sekvence jsou tak rychle viditelné.

Než budeš zasahovat do parseru, potřebuješ debugovací setup, který ti zobrazí skutečnou posloupnost bytů. Pro support a provoz to má zlatou hodnotu, protože později jasně určíš, zda protistrana posílá špatná data, nebo zda tvoje pipeline špatně dekóduje.

1) Zkontrolovat první bajty (BOM, začátek JSON)

Pokud JSON přichází s BOM, na začátku streamu uvidíš EF BB BF. Hned za tím by typicky mělo následovat „{“ nebo „[“. Pokud se ve stringu už objevuje „“, BOM nebyl zpracován jako BOM, ale byl dekódován jako text.

2) Logovat surové bajty v hexu – aber begrenzt

Nezaznamenávej celé payloady v produkci (ochrana údajů, náklady, objem logů). Osvědčilo se:

  • prefix (např. prvních 256 nebo 1024 bajtů),
  • suffix (posledních 256 bajtů),
  • a hash (SHA-256) pro korelaci, pokud potřebuješ payloady porovnávat.

Díky tomu často během minut identifikuješ problémy se speciálními znaky: Je posloupnost bajtů pro „ä“ správná (C3 A4)? Je přítomná truncace? Objevuje se neočekávané 0x00 (nullbyte), např. v důsledku mylného předpokladu UTF-16?

3) Content-Type und Charset mitloggen

Při HTTP/REST: zaznamenej Content-Type a deklarované charset. Pokud jsou bytové údaje zjevně v UTF-8, ale charset tvrdí něco jiného, klient by tomu neměl slepě důvěřovat. Pro JSON je UTF-8 de-facto standard. V pochybnostech: analýza bytů má přednost před hlavičkou.

Rychlý stream-parser se System.JSON: návrh bez zbytečných kopií

Schematické znázornění pipeline ze streamu, UTF-8-dekódování, omezení velikosti a JSON-DOM-parsingu v Delphi
Jasná pipeline s limitem a explicitním UTF-8 odděluje transport, dekódování a parsování přehledně.

Praktický vzor je: čteš ze streamu do bytového bufferu, z tohoto bufferu vytvoříš řetězec pouze jednou v UTF-8 a předáš tento řetězec JSON-parseru. To není „streaming“ ve smyslu SAX, ale je to kontrolovaná, výkonná pipeline bez nečekaných změn kódování.

Důležité pro architekturu: navrhni funkci tak, aby na jednom místě centrálně rozhodovala:

  • Které pravidlo kódování platí (obvykle UTF-8, BOM volitelný)?
  • Jak velký smí být payload maximálně (ochrana proti DoS, provozní limit)?
  • Jak vypadají chybové hlášky (s kontextem, ale bez úniku dat)?

Strategie čtení: omezené bufferování místo „StreamToString“ bez limitu

Pokud přijímáš JSON z externích zdrojů (partneři, mobilní klienti, třetí strany), je limit velikosti povinný. Bez limitu stačí jeden nešťastný request, aby službu dostal do vysokého paměťového tlaku. Prakticky to znamená: při čtení počítat součet bytů a při překročení hranice přerušit — s jasnou výjimkou, která je srozumitelná v monitoringu.

Proč často očekávám u UTF-8 „bez BOM“, ale BOM bych toleroval

U payloadů pro REST se BOM vyskytuje zřídka. U souborů (exporty, ruční úpravy) naopak častěji. Pro robustní importní toky je rozumné BOM tolerovat, ale zaznamenat ho do logu, protože může indikovat „svět souborů“ místo „světa API“.

Tichí zabijáci: výchozí nastavení TStreamReader a textové čtečky v kombinaci

TStreamReader je pohodlný, ale musíš mít dvě věci pevně pod kontrolou:

  • Explicitně nastavit kódování: Nespoléhat na to, že ho „rozpozná“.
  • Pochopit bufferování: Reader interně bufferuje. Pokud ten samý stream později čteš jinde, je důležitá aktuální pozice. To zní triviálně, ale v rozsáhlejších importních pipelinách se z toho rychle stává zdroj chyb.

Zejména nepříjemný je smíšený provoz: nejprve část číst jako byty (např. pro logování nebo magic-byty), potom pokračovat s TStreamReader. Pokud při tom neprovedete přesné navrácení pozice nebo neinicializujete reader na správném místě, přečtete od bytu 257 místo od 0. JSON-parser pak hlásí „Invalid character at position …“, i když je payload sám o sobě v pořádku.

Když diakritika i přes UTF-8 zůstávají „poškozené“: escapování vs. skutečný Unicode

JSON může obsahovat speciální znaky dvěma způsoby:

  • Jako skutečné UTF-8 znaky (např. „München“ jako byty C3 BC …).
  • Jako escape sekvence (např. „Mu00fcnchen“).

Obojí je platné. Pro praxi je důležité: escape sekvence obcházejí mnoho problémů s přenosem, ale jen zdánlivě zakrývají chyby kódování. Pokud tvůj systém někde interpretuje bajty chybně, je to provozní riziko, ne jen kosmetická závada. Navíc mohou escape sekvence při logování/monitoringu působit matoucím dojmem, pokud týmy očekávají „čitelný text“.

System.JSON ti v obou případech nakonec dodá běžné Delphi-řetězce (UTF-16), pokud byla cesta až tam korektní.

Realistické posouzení výkonu: náklady DOM, velká pole a selektivita

Největší páka pro výkon často není „udělat parser rychlejší“, ale parsovat méně. Se System.JSON je to těžké, protože získáš DOM. Tři typické situace:

  • Velká pole (10 000+ prvků): Vytvoření DOM stojí čas a RAM. Pokud potřebuješ jen 2 pole na prvek, je často rozumnější parser s podporou streamingu (SAX/Tokenizer). System.JSON k tomu není určen.
  • Jednotlivé objekty s mnoha poli: Pokud potřebuješ jen pár polí, můžeš DOM stále použít, ale vyhýbej se opakovanému průchodu. Získej hodnoty jednou a přiřaď je do svých struktur.
  • Více velkých payloadů za sebou: V importních úlohách nebo Sync-Workeru se vyplatí parsování uzavřít do jasného kroku a po každém dokumentu uvolnit všechny reference, aby Memory-Manager mohl uklidit. To je banální, ale ve službách se to rádo dělá „mimochodem“.

„Rychlý stream-parser“ s System.JSON je proto často dobrý kompromis: čtení efektivně a korektně, vědomé využití DOM, definování mezí. Pokud potřebuješ skutečnou streamingovou sémantiku (např. zpracovávat prvky pole po jednom bez držení všeho v paměti), není System.JSON správný základ.

Robustnost v provozu: typy chyb a jak je okamžitě učinit uchopitelnými

Chyby parsování JSON jsou v logách často málo užitečné, protože uvádějí pouze pozici. Pro provoz a support potřebuješ kontext:

  • Pozice v bajtech vs. pozice ve znacích: U UTF-8 to není totožné. Pokud parser hlásí pozici ve znacích, může se pozice v bajtech lišit. Pro výpisy bajtů je rozhodující pozice v bajtech.
  • Úryvek kolem místa chyby: Při chybě zaznamenejte malé okno kolem pozice (např. 40 znaků před/po), ale pouze pokud neobsahuje citlivá data. Alternativně: logujte jen hexadecimálně.
  • Korelace: Request-ID, Endpoint, Partner-ID, Payload-Hash. Jinak tu „jednu chybu“ už nikdy znovu nenajdete.

Cílem je, abys po produkčním incidentu během několika minut dokázal odpovědět: „kódování chybně interpretováno“, „payload odříznut“, „server vrací invalid JSON“ nebo „máme problém s mapováním“.

Jak cíleně vyhnout úskalím UTF-8: kontrolní seznam

  • Vždy explicitně uvádějte kódování: Při čtení ze streamu a při zápisu do logů/souborů se nespoléhejte na výchozí hodnoty.
  • Žádné převádění chunků na string: Pokud čteš po částech (chunked), sbírej bajty nebo použij dekodér, který multibajtové sekvence vyrovná.
  • Tolerovat BOM, ale mít jej viditelný: Akceptovat, ale umět jej při debugování rozpoznat.
  • Nastavte limity: Max. velikost payloadu, max. hloubka objektů/polí (pokud to můžete kontrolovat), timeouty v HTTP klientovi.
  • Oddělené odpovědnosti: odděleně kapsulovat „čtení transportu“ a „parsování JSON“. Tak se ladí rychleji a později lze vyměnit parser.
  • Kdy se námaha věnovaná streamovému parseru skutečně vyplatí?

    Nemusíš optimalizovat každé místo s JSONem. Tento přístup se obvykle vyplatí, pokud platí alespoň jedno z následujícího:

    • Velké payloady (několik MB) se pravidelně vyskytují nebo se mohou vyskytnout.
    • Dlouho běžící procesy (Service, Worker) zpracovávají mnoho payloadů a pozoruješ skoky v paměti nebo fragmentaci.
    • Interop s heterogenními systémy: několik partnerů, různé platformy, občas chybné kódování.
    • Historie incidentů: již byly „poškozené umlauty“, sporadické parse-chyby nebo těžko reprodukovatelné přerušení importu.

    Pokud jsou tvoje payloady malé a pocházejí z kontrolovaného zdroje, často stačí jednoduchý přístup – ale i tehdy: explicitně nastavit UTF-8 stojí téměř nic a zabrání pozdějším překvapením.

    Vymezení: Kdy potřebuješ skutečné streamování

    System.JSON je orientován na DOM. Pokud chceš data skutečně zpracovávat „za běhu“, např. velké pole prvek po prvku, aniž bys ho držel celé v paměti, potřebuješ jiný přístup k parseru (Tokenizer/SAX). To není hodnotící výrok, ale architektonické rozhodnutí:

    • DOM (System.JSON): pohodlný, vhodný pro typické business objekty, ale náročný na paměť.
    • Streaming/SAX: nižší nároky na paměť, vhodné pro velmi velká data, ale větší nároky na implementaci a pečlivější ošetření chyb.

    Smysluplný kompromis často bývá: solidně implementovat zpracování streamu, kódování a limity, a teprve potom se rozhodnout, zda DOM stále vyhovuje. V mnoha projektech tím už provoz výrazně stabilizuje.

    Závěr: JSON in Delphi bude spolehlivý, když budeš kódování a streamy řešit jako samostatnou vrstvu

    Většina problémů okolo „JSON in Delphi“ nesouvisí samotným JSON-parserem, ale s nenápadnou částí před ním: bajty ze streamu se převádějí na text. Pokud tam explicitně řešíš UTF-8, rozpoznáš případy s BOM, neignoruješ hranice chunků a nastavíš jasné velikostní limity, typické chyby se znaky zmizí – a sporadické parse-chyby se stanou reprodukovatelnými.

    System.JSON zůstává pragmatickým standardem: není nejrychlejší streamingový parser, ale je solidní, pokud kontroluješ vstup a vědomě akceptuješ náklady DOMu. Pokud chceš, můžeme společně projít tvou konkrétní import-/REST-cestu a identifikovat místo, kde kódování nebo chunking selhává: kontaktovat nás.

    Pro toto téma jsou důležité i JSON Stream Parser. Článek tyto aspekty jasně zařadí a ukazuje, na co se v praxi zaměřit.

    Projednat projekt nebo modernizační záměr s Net-Base.

    další krok

    Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz posuzovány společně již v rané fázi.

    Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.

    • Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
    • REST, přístup k datům, portály a rollout nebudou přesunuty do pozdějších fází.
    • Včas zjistíte, která varianta je ekonomicky i provozně životaschopná.

    Sdílet příspěvek

    Sdílet tento příspěvek přímo

    LinkedIn, X, XING, Facebook, WhatsApp a e-mail jsou ihned k dispozici. Pro Instagram připravíme odkaz a krátký text.

    E-mail

    Instagram se otevře v nové záložce. Odkaz a krátký text budou předtím zkopírovány do schránky.