Net-Base Časopis

13.08.2026

JSON u Delphi: Brzi stream-parser s System.JSON + zamke UTF-8 kod posebnih znakova

Wenn JSON-Payloads in Delphi direkt aus einem Stream kommen, wird aus „mal eben parsen“ schnell ein Produktionsproblem: hoher Speicherbedarf, sporadische Parse-Fehler und kaputte Umlaute. Dieser Praxisbeitrag zeigt, wie du mit System.JSON einen schnellen Stream-Parser aufbaust...

13.08.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

„JSON in Delphi“ zvuči kao riješen problem: System.JSON je uključen, REST-pozivi vraćaju tekst, gotovo. U praksi se pravi problemi pojavljuju tamo gdje JSON nije dostupan kao zgodan String, nego kao Stream: HTTP-Response-Stream, Datei-Stream, Named Pipe, Message-Queue ili veliki BLOB iz baze podataka. Tada se spoje tri stvari koje se u svakodnevnom radu često podcjenjuju: ponašanje memorije, kodiranje znakova (osobito UTF-8) i rubni slučajevi vezani uz posebne znakove.

Ovaj članak pokazuje čist, brz pristup za parsiranje JSON-a iz jednog TStream-a bez stvaranja nepotrebnih kopija – i prije svega bez tipičnih UTF-8 zamki zbog kojih se dijakritički znakovi „pokvare“ ili parseri povremeno izlaze s kriptičnim porukama. Fokus je na utjecaju na rad i stabilnost sučelja: reproducibilno debugiranje, jasne granice pristupa i kriteriji kada se troškovi doista isplate.

Zašto streamovi kod parsiranja JSON‑a u Delphi ponašaju drugačije

Dok god je JSON-dokument mali, put „čitati Stream u String, pa parsirati“ je praktičan. Ipak, iznad određene veličine payloada (tipično: velike liste, izvještaji, sinkronizacijski podaci, izvoz logova) to postaje skupo:

  • Duplikati memorije: Čitaš bajtove u buffer, pretvaraš u Unicode-String (Delphi-String = UTF-16), parser interno stvara dodatne strukture. To može kratkoročno značiti više kopija.
  • GC/Heap-pritisak: Mnogi privremeni Strings i JSON-vrijednosti povećavaju fragmentaciju i overhead alokacija, posebno u Longrunning-Prozessen (Services, Worker, Import-Jobs).
  • Slika greške postaje nejasna: Ako se pri čitanju pogrešno pretpostavi enkodiranje, JSON-parser vidi samo „čudne znakove“ ili neočekivane kontrolne bajtove.

Važno je jasna separacija: JSON je formalno Unicode, na prijenosu je gotovo uvijek UTF-8. Delphi međutim interno radi s UTF-16. Prijelaz od bajtova (Stream) do znakova (String) je mjesto gdje nastaju problemi sa posebnim znakovima – ne u samom JSON-u.

System.JSON: Što radi dobro – i gdje trebaš paziti

System.JSON je u Delphi standard za DOM-bazirani JSON: dobiješ objektni model (TJSONObject, TJSONArray), možeš čitati vrijednosti, iterirati, serializirati. To je robustno za tipične poslovne integracije, ali ima dvije posljedice:

  • Nije pravi streaming-parser: Objektni model se u potpunosti izgradi. Iako možda uštediš učitavanje u dodatni String, DOM i dalje ostaje memorijski intenzivan.
  • Ulaz parsera je u pravilu tekst: Ovisno o Delphi-verziji i korištenom API-ju brzo se opet vratiš na String, uključujući konverziju enkodiranja.

Ako je tvoj cilj „brzi Stream-Parser“, u praksi obično misliš na jedno od dva: (1) nema nepotrebnih kopija i (2) moguće rano fail-fast kod oštećenih payloada. Oba su moguća sa System.JSON, pod uvjetom da kontroliraš razinu pretvorbe bajtova u tekst.

UTF-8-zamke kod posebnih znakova: tipični uzroci

Apstraktna grafika prikazuje UTF-8 višebajtne znakove preko granica chunkova i ispravno međuspremanje u dekoderu prije JSON-parsanja
Chunking je bezopasan – pod uvjetom da UTF-8 dekoder međuspremi višebajtne sekvence preko granica.

Ako dijakritički znakovi (ä/ö/ü/ß) ili drugi posebni znakovi u rezultatu izgledaju krivo (ä, – itd.), to je gotovo uvijek problem neusklađenosti enkodiranja. U okruženju Delphi ti su uzroci posebno česti:

1) ANSI-Fallback durch „bequeme“ Helper

Neki načini čitanja implicitno prihvaćaju sistemsko ANSI-enkodiranje (codepage sustava Windows) ako nije proslijeđena eksplicitna kodiranje. To se primijeti tek kad payload ne sadrži samo ASCII. U testnim podacima često slučajno „prođe“, u produkciji se to pokaže na stvarnim imenima, lokacijama i slobodnom tekstu.

2) BOM-Verwirrung (Byte Order Mark)

UTF-8 može početi s BOM-om (bytes EF BB BF). U web-kontekstu BOM je prilično neuobičajen, ali se pojavljuje u datotekama. Neki čitači detektiraju BOM i prilagode enkodiranje, drugi ne ili samo u određenim načinima rada. Ako se BOM progura kao normalan znak u string, često vidiš nevidljivi „Zero Width No-Break Space“ na početku ili JSON-parser odmah zakaže na prvom tokenu.

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

Klasičan obrazac pogreške „ä“ umjesto „ä“ nastaje kad su UTF-8 bajtovi prvo ispravno dekodirani u Unicode, ali kasnije ponovno pogrešno interpretirani kao ANSI/UTF-8 bajtovi (ili obrnuto). U Delphi se to često događa kad je pretvorba između TBytes, RawByteString i string nejasna.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF-8 kodira posebne znakove u 2–4 bajta. Ako čitaš u chunkovima (npr. 8 KB) i granica chunka padne usred znaka, dekoder mora uredno međuspremiti preostale bajtove. Naivan pristup koji svaki chunk zasebno pretvara u string i potom ih spaja proizvodi nevažeće sekvence. To se može manifestirati kao „sporadična“ greška, ovisno o granicama paketa, ponašanju proxyja ili HTTP-chunkingu.

5) Falsche Annahmen aus HTTP-Headern

Kod REST izvor često navodi Content-Type: application/json; charset=utf-8. Međutim, neki serveri ne šalju charset, a neki šalju netočne podatke. Ako bez provjere koristiš zaglavlje, ponašanje može varirati ovisno o verziji backenda. Za operacije i podršku korisno je provjeriti stvarni tok bajtova i u slučaju greške zabilježiti ga u logu.

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

Robusna pipeline sastoji se od tri jasne faze:

  1. Čitanje bajtova iz streama (kontrolirano, po potrebi s limitom/timeoutom u HTTP-klijentu).
  2. Dekodiranje u Unicode s eksplicitnim UTF-8 (BOM opcionalno tolerirati).
  3. Parsiranje sa System.JSON u objektni model ili ciljanu ekstrakciju.

Najvažnija poluga je faza 2: ne želite da negdje „Default Encoding“ odlučuje. U Delphi to znači: TEncoding.UTF8 explizit setzen und nicht auf implizite Konvertierungen vertrauen.

Was „schnell“ hier konkret bedeutet

S System.JSON-om nećeš „optimizirati“ DOM. Ali možeš izbjeći:

  • dodatnu kopiju cijele payload‑poruke kao privremeni string, ako interno ionako trebaš samo nekoliko vrijednosti (tad se više isplati drugi parser; o tome kasnije),
  • višestruko prekodiranje,
  • i možeš vrlo velike payloadove kontrolirano učitavati (s ograničenjem veličine i jasnom porukom o pogrešci), umjesto da završe s Out‑of‑Memory ili Access Violations.

Der konkrete Randfall: Sonderzeichen kaputt, aber nur manchmal

Jedan praktičan rubni slučaj je posebno neugodan: payload je u osnovi valjani UTF-8‑JSON, ali ga čitaš u chunkovima i svaki chunk konvertiraš u string. Dok se pojavljuje samo ASCII ne primijetiš ništa. Čim neki umlaut padne točno na granicu chunka, nastaju nevažeće UTF‑8 sekvence. Ishod: ili pokvareni znakovi ili parse‑pogreška na mjestu koje nema veze s pravim sadržajem.

Kako to prepoznati:

  • Parse‑pogreške se pojavljuju „nasumično“ kod velikih odgovora, ne kod malih.
  • Isti request ponekad prođe, ponekad ne (ovisno o chunkiranju/transportu).
  • Hexdump bajtova pokazuje valjani UTF‑8, ali zabilježeni string sadrži Replacement Characters (�) ili klasični Mojibake.

Rješenje nije „više readln“ ili „veći buffer“ nego koristiti decoder koji ispravno buferira multibajtne sekvence preko granica chunkova. To je upravo trenutak kad TStreamReader u kombinaciji s UTF‑8‑Encoding može pomoći – pod uvjetom da ga ispravno inicijaliziraš.

Praxisleitfaden: UTF-8 in Delphi reproduzierbar prüfen

Debugging‑postavka s ispisima hexdumpova i radnim materijalom za provjeru UTF‑8 bajtova i BOM u JSON‑payloadovima
Za čisto debugiranje prvo je bitan byte‑stream: BOM, trunciranje i nevažeće sekvence tako se brzo uoče.

Prije nego što kreneš dirati parser, trebaš debug‑postavku koja ti prikazuje stvarni slijed bajtova. Za podršku i operacije to je neprocjenjivo, jer ćeš kasnije moći jasno reći isporučuje li suprotna strana krive podatke ili ih tvoja pipeline krivo dekodira.

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

Ako JSON dolazi s BOM‑om, na početku streama vidiš EF BB BF. Odmah nakon toga tipično treba doći „{“ ili „[“. Ako u stringu već stoji „“, onda BOM nije tretiran kao BOM nego je dekodiran kao tekst.

2) Rohbytes in Hex loggen – aber begrenzt

Ne logiraj kompletne payloadove u produkciji (zaštita podataka, troškovi, volumen logova). Dokazano korisno je:

  • prefiks (npr. prvih 256 ili 1024 bajta),
  • sufiks (posljednjih 256 bajta),
  • i hash (SHA‑256) za korelaciju, ako trebaš uspoređivati payloadove.

Uz to često u par minuta razlučiš probleme sa posebnim znakovima: je li bajt‑niz za „ä“ ispravan (C3 A4)? Radi li se o trunciranju? Pojavljuje li se neočekivani 0x00 (null‑bajt), npr. zbog pogrešne pretpostavke UTF‑16?

3) Content-Type und Charset mitloggen

Pri HTTP/REST: zabilježi Content-Type i deklarirani charset. Ako su bajtovi očito UTF-8, ali charset tvrdi nešto drugo, klijent ne bi trebao slijepo vjerovati. Za JSON je UTF-8 de-facto standard. U sumnji: analiza bajtova nadjačava zaglavlje.

Brzi Stream-Parser s System.JSON: Dizajn bez nepotrebnih kopija

Šematski prikaz pipelinea: Stream, UTF-8-dekodiranje, ograničenje veličine i JSON-DOM parsiranje u Delphi
Jasna pipeline s limitom i eksplicitnim UTF-8 jasno odvaja transport, dekodiranje i parsiranje.

Praktičan obrazac je: čitaš iz streama u byte-bufer, iz njega izgradiš string točno jednom s UTF-8 i proslijediš taj string JSON-parseru. To nije „Streaming“ u smislu SAX, ali je to kontrolirana, visokoperformansna pipeline bez iznenadnih promjena enkodiranja.

Važno za arhitekturu: izgradi funkciju tako da centralno na jednom mjestu odlučuje:

  • Koje pravilo enkodiranja vrijedi (obično UTF-8, BOM opcionalno)?
  • Kolika maksimalna veličina payloada je dopuštena (DoS-zaštita, operativna granica)?
  • Kako izgledaju poruke o pogrešci (s kontekstom, ali bez curenja podataka)?

Strategija čitanja: ograničeno bufferiranje umjesto „StreamToString“ bez limita

Ako prihvaćaš JSON iz vanjskih izvora (partneri, mobilni klijenti, treće strane), ograničenje veličine je obavezno. Bez limita jedan nesretan zahtjev može dovesti servis u stanje visokog opterećenja memorije. U praksi to znači: pri čitanju brojati zbroj bajtova i prekinuti nakon granice — s jasnim izuzetkom koji je čitljiv u monitoringu.

Zašto kod UTF-8 često očekujem „bez BOM-a“, ali bih BOM tolerirao

BOM je rijetko prisutan u REST-payloadima. U datotekama (eksporti, ručna obrada) pojavljuje se češće. Za robusne importne tokove smisleno je tolerirati BOM, ali ga učiniti vidljivim u logu, jer može ukazivati na „svijet datoteka“ umjesto „svijeta API-ja“.

Tihi ubojice: TStreamReader-zadane postavke i Text-Reader u miješanom radu

TStreamReader je praktičan, ali moraš čvrsto kontrolirati dvije stvari:

  • Izričito postaviti enkodiranje: Ne očekuj da će ga „prepoznati”.
  • Razumjeti bufferiranje: Reader interno buferira. Ako isti stream kasnije negdje drugdje ponovno čitaš, položaj je presudan. To zvuči trivijalno, ali u većim importnim pipelinama brzo postane izvor pogrešaka.

Posebno problematičan je miješani rad: prvo pročitati dio kao bajtove (npr. za logging ili magic-bajtove), a zatim nastaviti s TStreamReader. Ako pritom ne vratiš točno poziciju ili ne inicijaliziraš reader na ispravnoj poziciji, čitat ćeš od bajta 257 umjesto od 0. JSON-parser će tada prijaviti „Invalid character at position …“, iako je sama Payload ispravna.

Kad posebni znakovi unatoč UTF-8 ostaju „pokvareni“: Escaping vs. pravi Unicode

JSON može sadržavati posebne znakove na dva načina:

  • Kao pravi UTF-8 znakovi (npr. „München“ kao bajtovi C3 BC …).
  • Kao escape-sekvenca (npr. „Mu00fcnchen“).

Oboje je valjano. U praksi je važno: escape-sekvencije zaobilaze mnoge probleme prijenosa, ali samo prividno prikrivaju greške enkodiranja. Ako tvoj sustav negdje pogrešno interpretira bajtove, to je operativni rizik, a ne samo kozmetička greška. Osim toga, escape-sekvencije mogu zbuniti pri logiranju/monitoringu ako timovi očekuju „čitljiv tekst“.

System.JSON ti u oba slučaja na kraju isporučuje normalne Delphi-stringove (UTF-16), pod uvjetom da je put do tamo bio ispravan.

Realistična procjena performansi: troškovi DOM-a, veliki nizovi i selektivnost

Najveći utjecaj na performanse često nije „ubrzati parser“, nego manje parsirati. Sa System.JSON to je teško, jer dobivaš DOM. Tri tipične situacije:

  • Veliki nizovi (10.000+ elemenata): Izgradnja DOM-a košta vrijeme i RAM. Ako ti trebaju samo 2 polja po elementu, parser sposoban za streaming (SAX/Tokenizer) često je smisleniji. System.JSON nije za to namijenjen.
  • Pojedinačni objekti s mnogo polja: Ako ti trebaju samo nekoliko polja, možeš i dalje koristiti DOM, ali izbjegavaj višestruko traversiranje. Dohvati vrijednosti jednom i mapiraj ih u svoje strukture.
  • Više velikih Payloada zaredom: U Import-Jobs ili Sync-Worker scenarijima isplati se kapsulirati parsiranje u jasan korak i nakon svakog dokumenta osloboditi sve reference, kako bi Memory-Manager mogao očistiti. To je banalno, ali u servisima se rado radi „usput“.

Ein „schneller Stream-Parser“ mit System.JSON ist daher oft ein guter Kompromiss: Einlesen effizient und korrekt, DOM bewusst nutzen, Grenzen definieren. Wenn du echte Streaming-Semantik brauchst (z. B. Array-Elemente nacheinander verarbeiten, ohne alles zu halten), ist System.JSON nicht die richtige Basis.

Robusnost u pogonu: obrasci pogrešaka i kako ih odmah učiniti dohvatljivima

Pogreške pri parsiranju JSON-a u logovima često nisu korisne jer navode samo poziciju. Za operacije i podršku treba ti kontekst:

  • Pozicija u bajtovima vs. pozicija u znakovima: Kod UTF-8 to nije isto. Ako parser prijavi poziciju u znakovima, pozicija u bajtovima može biti drugačija. Za byte-dumpove je presudna pozicija u bajtovima.
  • Isječak oko mjesta pogreške: U slučaju greške logiraj mali prozor oko pozicije (npr. 40 znakova prije/nakon), ali samo ako ne sadrži osjetljive podatke. Alternativno: logiraj samo heksadecimalno.
  • Korelacija: Request-ID, Endpoint, Partner-ID, Payload-Hash. Inače nikad nećeš ponovno pronaći „onu jednu grešku“.

Cilj je da nakon produkcijskog incidenta unutar nekoliko minuta možeš odgovoriti: „enkodiranje pogrešno interpretirano“, „Payload odsječen“, „server vraća nevaljan JSON“ ili „imamo problem s mapiranjem“.

Namjerno izbjegavanje zamki UTF-8: kontrolna lista

  • Uvijek eksplicitno odrediti encoding: Prilikom čitanja iz streama i prilikom zapisivanja u logove/datoteke ne oslanjaj se na zadane postavke.
  • Bez konverzije chunk→string: Ako čitaš u chunkovima, skupljaj bajtove ili koristi decoder koji bufferira multibajtne sekvence.
  • BOM tolerantan, ali vidljiv: Prihvati ga, ali moraš ga moći detektirati pri debugiranju.
  • Postavi limite: Max. veličina Payloada, max. dubina objekta/niza (ako to možeš kontrolirati), Timeouts u HTTP-klijentu.
  • Odvojene odgovornosti: „čitanje transporta“ i „parsiranje JSON‑a“ kapsulirati odvojeno. Tako se brže debugira i kasnije se može zamijeniti parser.
  • Wann lohnt sich der Aufwand für einen Stream-Parser wirklich?

    Ne moraš optimizirati svako mjesto gdje se pojavi JSON. Pristup se obično isplati kada vrijedi barem jedno od sljedećeg:

    • Veliki payloadi (nekoliko MB) pojavljuju se redovito ili mogu nastati.
    • Dugotrajni procesi (Service, Worker) obrađuju mnogo payloada i uočavaš memorijske skokove (Memory‑Spikes) ili fragmentaciju.
    • Interoperabilnost s heterogenim sustavima: više partnera, različite platforme, povremeno neispravna enkodiranja.
    • Povijest incidenata: već je bilo „pokvarenih dijakritičkih znakova“, sporadičnih grešaka parsiranja ili teško reproducirajućih prekida uvoza.

    Ako su tvoji payloadi mali i dolaze iz kontroliranog izvora, često je dovoljan jednostavan pristup – ali i tada: izričito postaviti UTF‑8 gotovo ništa ne košta i sprječava kasnije iznenađenja.

    Abgrenzung: Wenn du echtes Streaming brauchst

    System.JSON je orijentiran na DOM. Ako želiš podatke zaista „u prolazu“ obraditi, npr. veliko polje element po element bez držanja cijelog u memoriji, treba ti drugi pristup parsera (Tokenizer/SAX). To nije vrijednosni sud, nego arhitektonska odluka:

    • DOM (System.JSON): praktičan, dobar za tipične poslovne objekte, ali memorijski zahtjevan.
    • Streaming/SAX: niska potrošnja memorije, dobar za vrlo velike podatke, ali veći implementacijski napor i zahtjevnije upravljanje greškama.

    Često je razuman kompromis: uredno izgraditi rukovanje streamom, enkodiranje i limitiranja, pa tek onda odlučiti odgovara li DOM. U mnogim projektima upravo to značajno stabilizira rad u produkciji.

    Fazit: JSON in Delphi wird zuverlässig, wenn du Encoding und Streams als eigene Schicht behandelst

    Većina problema vezanih uz „JSON in Delphi“ nema veze s JSON‑parserom kao takvim, nego s neupadljivim dijelom prije njega: bajtovi iz streama pretvaraju se u tekst. Ako tamo izričito rukuješ UTF‑8, prepoznaješ slučajeve s BOM‑om, ne ignoriraš granice chunkova i postaviš jasne veličinske limite, tipične pogreške sa znakovima nestaju – a sporadične greške parsiranja postaju reproducibilne.

    System.JSON ostaje pragmatičan standard: nije najbrži streaming‑parser, ali je solidan ako kontroliraš ulaz i svjesno prihvaćaš troškove DOM‑a. Ako želiš, možemo zajedno proći tvoj konkretni Import-/REST‑put i identificirati mjesto na kojem enkodiranje ili chunking zakaže: Kontaktirajte nas.

    Za ovu temu važni su i JSON stream parseri. Članak kontekstualizira ove aspekte razumljivo i pokazuje na što treba paziti u praksi.

    Razgovarajte o projektu ili modernizacijskom pothvatu s Net-Base.

    sljedeći korak

    Ako se tema pretvori u stvarni projekt, arhitekturu, postojeće sustave i operacije trebalo bi rano zajednički razmotriti.

    Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

    • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
    • REST, pristup podacima, portali i rollout neće biti odgođeni kao naknadne posljedice.
    • Rano prepoznajete koji je put ekonomski i operativno održiv.

    Podijeli objavu

    Izravno proslijedite ovu objavu

    LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram odmah pripremamo poveznicu i kratak tekst.

    E-pošta

    Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.