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
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í:
- Číst bajty ze streamu (kontrolovaně, případně s limitem/timeoutem v HTTP klientu).
- Dekódování na Unicode s explicitním UTF‑8 (BOM lze volitelně tolerovat).
- 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
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í
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.
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.
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á.