A magazintémától a projektgyakorlatig
A bejegyzéshez tartozó szolgáltatási és technikai oldalak
„JSON in Delphi“ úgy hangzik, mintha megoldott probléma lenne: System.JSON beépítve, REST-hívások szöveget adnak vissza, kész. A gyakorlatban a valódi hibák azonban ott keletkeznek, ahol a JSON nem kényelmes Stringként áll elő, hanem Streamként: HTTP-Response-Stream, fájlstream, Named Pipe, Message-Queue vagy egy nagy BLOB az adatbázisból. Ilyenkor három, a mindennapokban gyakran alábecsült dolog találkozik: memóriahasználat, karakterkódolás (különösen UTF-8) és az ékezetekhez kapcsolódó szélső esetek.
Ez a bejegyzés egy tiszta, gyors megközelítést mutat be arra, hogyan parsoljunk JSON-t egy TStream-ből anélkül, hogy fölösleges másolatokat hoznánk létre – és ami fontosabb, hogyan kerüljük el a tipikus UTF-8-csapdákat, melyek során az umlautok „elromlanak” vagy a parszerek szakaszosan titokzatos hibákat dobálnak. A fókusz az üzemeltetésre és a felületstabilitásra gyakorolt hatásokon van: reprodukálható hibakeresés, a megközelítés határainak egyértelműsítése és azonosítási kritériumok, mikor éri meg valóban a plusz munka.
Miért másképp viselkednek a streamek a JSON-parsolásnál Delphi-ben
Amíg egy JSON-dokumentum kicsi, a „streamet beolvasom stringbe, aztán parse” út kényelmes. Egy bizonyos payload-méret fölött (jellemzően: nagy listák, reportok, szinkronizációs adatok, naplóexportok) ez azonban drágává válik:
- Memóriamásolatok: Beolvasod a bájtokat egy pufferbe, átalakítod Unicode-stringgé (Delphi-String = UTF-16), a parser belsőleg további struktúrákat hoz létre. Ez rövid távon több másolatot eredményezhet.
- GC/heap-nyomás: Sok ideiglenes String és JSON-érték növeli a fragmentációt és az allokációs overhead-et, különösen hosszú élettartamú folyamatoknál (Services, Worker, Import-jobok).
- A hibajelenség tisztázatlanná válik: Ha az olvasás során már rossz kódolási feltételezéseket teszünk, a JSON-parser csak „furcsa karaktereket” vagy váratlan vezérőbájtokat lát.
Lényeges a világos szétválasztás: a JSON formálisan Unicode, az átvitel jellemzően UTF-8. Delphi viszont belsőleg UTF-16-tal dolgozik. A bájtok (Stream) és a karakterek (String) közötti átmenet az a pont, ahol az ékezetekkel kapcsolatos problémák keletkeznek – nem magában a JSON-ban.
System.JSON: miben jó – és mire kell figyelni
System.JSON a Delphi-ben a DOM-alapú JSON szabványos eszköze: kapsz egy objektummodellt (TJSONObject, TJSONArray), lekérdezhetsz értékeket, iterálhatsz, serializálhatsz. Ez robusztus tipikus üzleti integrációkhoz, ugyanakkor két következménnyel jár:
- Nem igazi streaming-parser: Az objektummodell teljesen felépül. Lehet, hogy megspórolod a külön stringbe olvasást, de a DOM továbbra is memóriaigényes.
- A parser bemenete jellemzően szöveg: A Delphi-verziótól és az használt API-tól függően hamar visszakerülhetsz a Stringhez, beleértve a kódoláskonverziót is.
Ha a célod „gyors stream-parser”, a gyakorlatban általában két dolgot értesz alatta: (1) nincs fölösleges másolatképzés és (2) a hibás payloadoknál a lehető legkorábbi Fail-fast. Mindkettő elérhető System.JSON-szal, amennyiben kontroll alatt tartod a bájtból-szövegbe lépést.
UTF-8-csapdák ékezetes karaktereknél: a tipikus okok
Ha az umlautok (ä/ö/ü/ß) vagy más speciális karakterek az eredményben rosszul jelennek meg (ä, – stb.), az szinte mindig kódolási eltérés. A Delphi környezetben ezek az okok különösen gyakoriak:
1) ANSI-visszaesés a „kényelmes” helper-ek miatt
Néhány olvasási útvonal hallgatólagosan a rendszer ANSI-kódolását (a Windows rendszer codepage-je) veszi alapul, ha nincs explicit kódolás megadva. Ez csak akkor tűnik fel, amikor a payload nem csak ASCII-t tartalmaz. Tesztadatokban ez gyakran véletlenszerűen „oké”, éles környezetben viszont valódi nevek, helyek és szabad szövegek esetén problémát okoz.
2) BOM-zavar (Byte Order Mark)
Az UTF-8 BOM-mal kezdődhet (bájtok EF BB BF). Web-környezetben a BOM ritkább, fájloknál azonban előfordul. Néhány reader felismeri a BOM-ot és ennek megfelelően beállítja a kódolást, mások nem, vagy csak bizonyos módokban. Ha a BOM normál karakterként kerül a stringbe, gyakran egy láthatatlan „Zero Width No-Break Space” jelenik meg a legelején, vagy a JSON-Parser rögtön az első tokennél elbukik.
3) Dupla konvertálás (az UTF-8 „újra” értelmezése)
A klasszikus hibakép, ahol „ä” helyett „ä” jelenik meg, akkor alakul ki, ha az UTF-8 bájtokat először helyesen Unicode-ra dekódolták, majd később újra ANSI/UTF-8 bájtokként értelmezik (vagy fordítva). A Delphi környezetben ez gyakori, ha a TBytes, RawByteString és string közötti konverzió nem egyértelmű.
4) Levágás többbájtos karakter közepén
Az UTF-8 speciális karaktereket 2–4 bájtban kódolja. Ha chunkokban olvasol (pl. 8 KB), és a chunk-határ egy karakter közepére esik, a dekódernek tisztán kell pufferelnie. Egy naiv megközelítés, amely minden chunkot külön stringgé alakít és összefűz, érvénytelen sorozatokat eredményez. Ez „sporadikus” hibaként jelentkezhet, a csomaghatároktól, proxy-viselkedéstől vagy HTTP-chunkolástól függően.
5) Hibás feltételezések HTTP-fejlécek alapján
A REST esetén a forrás gyakran a Content-Type: application/json; charset=utf-8. Ugyanakkor egyes szerverek nem adnak meg charset-et, mások hibás értéket szolgáltatnak. Ha vakon a fejlécre hagyatkozol, az backend verziójától függően változhat. Üzemeltetés és support számára hasznos ellenőrizni a tényleges bájtfolyamot és hibák esetén logolni.
Egy tiszta megközelítés: Stream → UTF-8-dekóder → JSON-Parser
A robusztus pipeline három egyértelmű szakaszból áll:
- Bájtok olvasása a stream-ből (kontrolláltan, szükség esetén limit/timeout beállítással az HTTP-Clientben).
- Unicode-ra dekódolás explicit UTF-8 használatával (BOM opcionálisan tolerálható).
- Parsing a System.JSON használatával objektummodellbe vagy célzott kinyerésre.
A legfontosabb beavatkozási pont a 2. lépés: nem akarod, hogy valahol a „Default Encoding” döntsön. A Delphi esetén ez azt jelenti: TEncoding.UTF8 expliciten beállítani és nem implicit konverziókra támaszkodni.
Mit jelent itt konkrétan a „schnell”
A System.JSON használatával nem fogod „eloptimalizálni“ a DOM-ot. Viszont elkerülheted:
- az egész payload egy további másolatát köztes stringként, ha belsőleg amúgy csak néhány értékre van szükséged (ilyenkor inkább másik parser éri meg; erről később),
- többszörös átkódolást,
- és képes leszel nagyon nagy payloadokat kontrolláltan beolvasni (méretkorláttal és tiszta hibaüzenettel), ahelyett, hogy Out-of-Memory vagy Access Violations miatt végeznél.
Konkrét szélső eset: speciális karakterek hibásak, de csak néha
Egy gyakorlatból vett szélső eset különösen alattomos: a payload alapvetően érvényes UTF-8-JSON, de te chunko(k)ban olvasod, és chunkonként konvertálsz Stringgé. Amíg csak ASCII szerepel, semmi nem tűnik fel. Amint egy umlaut pontosan egy chunk-határon lesz, érvénytelen UTF-8 szekvenciák keletkeznek. Eredmény: vagy törött karakterek, vagy egy parse-hiba egy olyan helyen, ami nem illeszkedik a tényleges tartalomhoz.
Hogyan ismerheted fel:
- Parse-hibák „véletlenszerűen“ jelentkeznek nagy válaszoknál, nem a kicsiknél.
- Ugyanaz a request néha sikerül, néha nem (a chunkolástól/átviteltől függően).
- Egy hex-dump a byte-okról érvényes UTF-8-at mutathat, de a logolt stringed Replacement Characteröket (�) vagy klasszikus mojibake-ot tartalmaz.
A megoldás nem az, hogy „többet readln“-elsz vagy „nagyobb buffereket“ adsz, hanem egy olyan dekódert használsz, amely a multibyte-szekvenciákat a chunk-határokon átívelve helyesen puffereli. Pont ebben lehet hasznos TStreamReader UTF-8-kódolással kombinálva – feltéve, hogy helyesen inicializálod.
Gyakorlati útmutató: UTF-8 reprodukálható ellenőrzése az Delphi-ben
Mielőtt a parseren kezded állítgatni, szükséged van egy hibakereső beállításra, amely láthatóvá teszi a tényleges bájtsorrendet. Support és üzemeltetés szempontjából ez aranyat ér, mert később egyértelműen meg tudod mondani, hogy a partner hibás adatot küld-e, vagy a te csővezetéke decode-olja rosszul.
1) Az első bájtok ellenőrzése (BOM, JSON-kezdete)
Ha a JSON BOM-mal érkezik, a stream elején EF BB BF látszik. Rögtön ezután jellemzően „{“ vagy „[“ kell, hogy legyen. Ha a stringben már „“ látszik, a BOM-ot nem BOM-ként kezelték, hanem szövegként dekódolták.
2) Nyers bájtok hexben logolása – de korlátozva
Ne logolj teljes payloadokat éles környezetben (adatvédelem, költségek, log-méret). Bevált megoldások:
- Prefix (pl. az első 256 vagy 1024 bájt),
- Suffix (az utolsó 256 bájt),
- és egy hash (SHA-256) korrelációhoz, ha payloadokat kell összehasonlítanod.
Ezzel gyakran percek alatt behatárolhatod a speciális karakterek problémáját: a „ä“ bájtsorozata helyes-e (C3 A4)? Van-e troncírozás? Megjelenik-e egy váratlan 0x00 (nullbyte), például egy UTF-16 téves feltételezés miatt?
3) Content-Type und Charset mitloggen
HTTP/REST esetén: Naplózd a Content-Type-ot és a deklarált charset-et. Ha a bájtok egyértelműen UTF-8-asok, de a charset mást állít, a kliens ne kövesse vakon. JSON esetén az UTF-8 a de-facto szabvány. Kétség esetén: a bájtelemzés felülírja a header-t.
Gyors stream-elemző System.JSON-sel: tervezés felesleges másolatok nélkül
Egy gyakorlatias minta: a streamből bájtpufferbe olvasol, abból pontosan egyszer hozol létre egy Stringet UTF-8-cal, és ezt a Stringet adod át a JSON-parszernak. Ez nem „streaming” SAX-értelemben, de kontrollált, teljesítményorientált pipeline, amely nem okoz meglepetésszerű kódolásváltásokat.
Architektúra szempontjából fontos: építsd a függvényt úgy, hogy egyetlen központi helyen döntsön:
- Melyik kódolási szabály érvényes (alapértelmezésben UTF-8, BOM opcionális)?
- Mekkora lehet maximálisan a payload (DoS-védelem, üzemhatár)?
- Hogyan nézzenek ki a hibajelentések (kontextussal, de adatkiszivárgás nélkül)?
Beolvasási stratégia: korlátozott pufferelés a korlátlan „StreamToString” helyett
Ha külső forrásokból (partnerek, mobil kliens, harmadik fél) fogadsz JSON-t, a méretkorlát kötelező. Korlát nélkül egyetlen szerencsétlen kérés is Memory Pressure-t okozhat a szolgáltatásban. Gyakorlatban ez azt jelenti: az olvasásnál számold a bájtok összegét, és egy határ elérésekor szakítsd meg – egyértelmű Exception-nel, amely érthető a monitoring számára.
Miért várom gyakran az UTF-8-at „BOM nélkül”, de miért tolerálnám a BOM-ot
REST-payloadoknál a BOM ritka. Fájloknál (exportok, kézi szerkesztés) viszont gyakoribb. Robusztus importutaknál érdemes a BOM-ot tolerálni, de naplóban láthatóvá tenni, mert jelzés lehet arra, hogy „fájl-világ” helyett „API-világ” érkezett az adat.
A csendes gyilkosok: TStreamReader-alapértelmezések és a szövegolvasók vegyes használata
TStreamReader kényelmes, de két dolgot mindenképp szigorúan tarts kézben:
- Állítsd be explicit módon az encodinget: Ne bízz abban, hogy „felismeri“.
- Értsd a pufferelést: A reader belső pufferelést végez. Ha ugyanazt a streamet később máshol ismét olvasod, a pozíció számít. Ez triviálisnak hangzik, de nagyobb importpipeline-oknál gyorsan hibaforrás lesz.
Különösen kellemety a kevert használat: először egy részt bájtokként olvasol (pl. logolás vagy magic-byte ellenőrzés miatt), majd TStreamReader-rel folytatod. Ha nem állítod vissza rendesen a pozíciót, vagy nem a megfelelő helyen inicializálod a readert, előfordul, hogy byte 257-től olvasol a 0 helyett. A JSON-parszer ekkor „Invalid character at position …” hibát jelez, még ha a payload maga helyes is.
Ha a speciális karakterek UTF-8 mellett is „tönkremennek”: escapelés vs. valódi Unicode
A JSON kétféleképpen tartalmazhat speciális karaktereket:
- Mint valódi UTF-8 karakterek (pl. „München” bájtjai C3 BC …).
- Escape-szekvenciaként (pl. „Mu00fcnchen“).
Mindkettő érvényes. A gyakorlatban fontos: az escape-szekvenciák sok szállítási problémát megkerülnek, de csak látszólag fedik el az encoding-hibákat. Ha a rendszered valahol bájtokat értelmez rosszul, az üzemeltetési kockázat, nem csupán kozmetikai hiba. Emellett az escape-szekvenciák zavart okozhatnak a naplózás/monitorozás során, ha a csapatok „olvasható szöveget” várnak.
System.JSON mindkét esetben végül normális Delphi-stringeket (UTF-16) ad vissza, feltéve, hogy az odáig vezető út helyes volt.
Teljesítmény reálisan értékelve: DOM-költségek, nagy tömbök és szelektivitás
A legnagyobb teljesítmény-hatás gyakran nem az, hogy „a parser gyorsabb legyen”, hanem hogy kevesebbet parseolj. System.JSON esetén ez nehéz, mert a DOM-ot kapod. Három tipikus helyzet:
- Nagy tömbök (10.000+ elem): A DOM felépítése időt és RAM-ot igényel. Ha elemenként csak 2 mezőre van szükséged, gyakran érdemes streaming-képes parseert (SAX/Tokenizer) használni. System.JSON erre nincs tervezve.
- Egyedi objektumok sok mezővel: Ha csak néhány mező kell, használhatod a DOM-ot, de kerüld a többszöri bejárást. Olvasd ki az értékeket egyszer, és térképezd át őket a saját struktúráidba.
- Több nagy payload egymás után: Import-feladatokban vagy sync-worker esetén érdemes a parseolást egy jól körülhatárolt lépésbe kapszulázni, és minden dokumentum után felszabadítani az összes referenciát, hogy a memory manager el tudja végezni a takarítást. Ez banális, mégis szolgáltatásoknál gyakran „mellékesen” történik.
Egy „gyors stream-parser” System.JSON mellett ezért gyakran jó kompromisszum: hatékony és helyes beolvasás, a DOM tudatos használata, határok definiálása. Ha valódi streaming-szemantikára van szükséged (pl. tömbelemek soronkénti feldolgozása anélkül, hogy mindent megtartanál), a System.JSON nem a megfelelő alap.
Robusztusság az üzemeltetésben: hibaképek és hogyan tedd azonnal kézzelfoghatóvá
JSON-parse-hibák a logokban gyakran nem informatívak, mert csak egy pozíciót adnak meg. Az üzemeltetés és a support számára kontextusra van szükség:
- Bájtpozíció vs. karakterpozíció: UTF-8 esetén ezek nem azonosak. Ha a parser karakterpozíciót jelöl, a bájtpozíció eltérhet. Bájtdumpokhoz a bájtpozíció döntő.
- Snippet a hibapozíció körül: Hibánál naplózz egy kis ablakot a pozíció körül (pl. 40 karakter előtte/utána), de csak akkor, ha nincs érzékeny adat. Alternatívaként: naplózz csak hexadecimálisan.
- Korreláció: Request-ID, Endpoint, Partner-ID, Payload-Hash. Ellenkező esetben soha nem találod meg „az egy hibát” újra.
A cél, hogy egy produkciós incidens után perceken belül meg tudd válaszolni: „kódolás helytelenül értelmezve”, „payload le lett vágva”, „szerver invalid JSON-t szolgáltat” vagy „térképezési probléma van”.
UTF-8-buktatók célzott elkerülése: Ellenőrzőlista
- Kódolás mindig explicit: Streamből olvasáskor és logok/fájlok írásakor ne bízz az alapértelmezésekben.
- Ne konvertálj chunk-ot stringgé: Ha chunkelt olvasol, gyűjts bájtokat vagy használj egy dekódert, amely puffereli a multibájtos szekvenciákat.
- BOM-toleráns, de látható: Fogadd el, de legyen detektálható hibakereséskor.
- Korlátok beállítása: max. payload-méret, max. objektum-/tömbmélység (ha tudod kontrollálni), időtúllépések a HTTP-kliensekben.
Mikor éri meg valóban a befektetés egy Stream-Parserbe?
Nem kell minden JSON-helyet optimalizálni. A megközelítés jellemzően akkor éri meg, ha legalább az alábbiak közül egy fennáll:
- Nagy payloadok (több MB) rendszeresen előfordulnak vagy előfordulhatnak.
- Hosszan futó folyamatok (Service, Worker) sok payloadot dolgoznak fel, és memóriaspikeket vagy fragmentációt tapasztalsz.
- Interoperabilitás heterogén rendszerekkel: több partner, különböző platformok, időnként hibás kódolások.
- Incidens-história: már voltak „tönkrement ékezetek”, időszakos parse-hibák vagy nehezen reprodukálható importmegszakadások.
Ha a payloadjaid kicsik és ellenőrzött forrásból származnak, gyakran elegendő egy egyszerű megoldás – de még akkor is: az explicit UTF-8 beállítás alig kerül valamibe, és későbbi meglepetéseket előz meg.
Elhatárolás: amikor valódi streamingre van szükség
A System.JSON DOM-orientált. Ha az adatokat tényleg „átfolyás közben” akarod feldolgozni, például egy nagy tömböt elemenként anélkül, hogy teljesen a memóriában tartanád, más parser-megközelítésre van szükség (Tokenizer/SAX). Ez nem értékítélet, hanem architekturális döntés:
- DOM (System.JSON): kényelmes, tipikus üzleti objektumokhoz alkalmas, de memóriaigényes.
- Streaming/SAX: alacsony memóriaigény, nagyon nagy adatokhoz jó, de több implementációs munka és gondosabb hibakezelés szükséges.
Gyakran értelmes kompromisszum: a stream-kezelést, a kódolást és a korlátokat tisztán megépíteni, és csak ezután dönteni, hogy a DOM még megfelel-e. Sok projektben már ez önmagában jelentősen stabilizálja az üzemet.
Következtetés: JSON in Delphi megbízható lesz, ha a kódolást és a streameket külön rétegként kezeled
A „JSON in Delphi” körüli problémák többsége nem maga a JSON-parser miatt van, hanem az előtte lévő észrevétlen szakasz miatt: a streamből érkező byte-ok szöveggé alakulnak. Ha ott explicit kezelsz UTF-8-at, felismered a BOM-eseteket, nem hagyod figyelmen kívül a chunk-határokat és tiszta méretkorlátokat állítasz, a tipikus speciális karakterekkel kapcsolatos hibák eltűnnek – és az időszakos parse-problémák reprodukálhatóvá válnak.
A System.JSON továbbra is pragmatikus szabvány: nem a leggyorsabb streaming-parser, de szilárd, ha kontrollálod a bemenetet és tudatosan elfogadod a DOM-költségeket. Ha szeretnéd, közösen végig tudjuk venni a konkrét Import-/REST-útvonaladat és azonosítjuk azt a pontot, ahol a kódolás vagy a chunking felborul: vedd fel velünk a kapcsolatot.
Ehhez a témához a JSON Stream Parser-ek is fontosak. A bejegyzés ezeket az aspektusokat érthetően rendezi, és megmutatja, mire kell figyelni a gyakorlatban.
Projekt vagy modernizációs feladat megbeszélése Net-Base-vel.
Következő lépés
Ha a téma valós projektté válik, az architektúrát, a meglévő rendszert és az üzemeltetést már korán együtt kell értékelni.
Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.
- A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
- REST, az adathozzáférés, a portálok és a Rollout nem kerülnek utólagos teendőkként elhalasztásra.
- Már korán láthatja, melyik út gazdaságilag és üzemeltetési szempontból életképes.