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 prisutan, REST-pozivi vraćaju tekst, gotovo. U praksi se stvarne greške javljaju tamo gdje JSON nije dostupan kao zgodan String, nego kao Stream: HTTP-Response-Stream, datotečni stream, Named Pipe, Message-Queue ili veliki BLOB iz baze podataka. Tada se spoje tri faktora koja se u svakodnevici često potcjenjuju: ponašanje memorije, enkodiranje znakova (posebno UTF-8) i rubni slučajevi vezani uz posebne znakove.
Ovaj članak prikazuje čist, brz pristup za parsiranje JSON-a iz TStream bez stvaranja nepotrebnih kopija – i prije svega bez tipičnih zamki UTF-8 zbog kojih se dijakritički znakovi ponekad „pokvare“ ili parseri sporadično padnu s kriptičnim porukama. Fokus je na posljedicama za operacije i stabilnost sučelja: reproducibilno debugiranje, jasne granice pristupa i kriteriji kada se dodatni napor doista isplati.
Warum Streams beim JSON-Parsing in Delphi anders ticken
Dok je JSON-dokument mali, put „čitati stream u String, pa onda parsirati“ je praktičan. Međutim, iznad određene veličine payloada (tipično: velike liste, izvještaji, sinkronizacioni podaci, izvoz logova) to postaje skupo:
- Duplikati u memoriji: Čitaš bajtove u bafer, pretvaraš ih u Unicode-String (Delphi-String = UTF-16), parser interno gradi dodatne strukture. To može kratkoročno značiti više kopija.
- GC/Heap pritisak: Mnogobrojni privremeni stringovi i JSON-vrijednosti povećavaju fragmentaciju i overhead alokacija, posebno u dugotrajnim procesima (servisi, workeri, import poslovi).
- Slika greške postaje nejasna: Ako pri čitanju budu donesene pogrešne pretpostavke o enkodiranju, JSON-parser vidi samo „čudne znakove“ ili neočekivane kontrolne bajtove.
Važno je jasna razdvojenost: JSON je formalno Unicode, na prijenosnom sloju je gotovo uvijek UTF-8. Delphi međutim interno radi s UTF-16. Prijelaz od bajtova (Stream) do znakova (String) je mjesto na kojem nastaju problemi sa posebnim znakovima – ne u samom JSON-u.
System.JSON: Was es gut kann – und wo du aufpassen musst
System.JSON je u Delphi standard za DOM-bazirani JSON: dobiješ objektni model (TJSONObject, TJSONArray), možeš dohvatiti vrijednosti, iterirati i serializirati. To je robusno za tipične poslovne integracije, ali ima dvije posljedice:
- Nije pravi streaming-parser: Objektni model se gradi u potpunosti. Možda uštediš čitanje u dodatni string, ali DOM i dalje troši značajnu memoriju.
- Parser-ulaz je u pravilu tekst: Ovisno o verziji Delphi i korištenom API-ju, brzo se opet vratiš na string, uključujući konverziju enkodiranja.
Ako ti je cilj „brzi stream-parser“, u praksi obično misliš na jedno od dva: (1) izbjegavanje nepotrebnih kopija i (2) što ranije fail-fast za oštećene payload-e. Oba su moguća s System.JSON, pod uvjetom da kontroliraš korak pretvorbe bajtova u tekst.
UTF-8 zamke kod posebnih znakova: tipični uzroci
Ako se Umlaute (ä/ö/ü/ß) ili drugi specijalni znakovi u rezultatu prikazuju pogrešno (ä, – itd.), to je gotovo uvijek problem sa enkodiranjem. U Delphi okruženju ovi uzroci su posebno česti:
1) ANSI-Fallback durch „bequeme“ Helper
Neki načini čitanja tihо pretpostave sistemsko ANSI-enkodiranje (codepage Windows-sistema) ako se ne proslijedi eksplicitna kodna stranica. To postane vidljivo tek kada payload sadrži više od ASCII. U testnim podacima je to često slučajno „u redu“, u produkciji dovodi do problema kod stvarnih imena, lokacija i slobodnih tekstova.
2) BOM-Verwirrung (Byte Order Mark)
UTF-8 može početi s BOM-om (bajtovi EF BB BF). U web-kontekstu BOM je rijedak, ali se javlja u datotekama. Neki čitači prepoznaju BOM i prilagode enkodiranje, drugi ne ili to rade samo u određenim režimima. Ako BOM dospije u string kao običan znak, često ćeš vidjeti 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čni obrazac greške „ä“ umjesto „ä“ nastaje kada UTF-8-bajtovi najprije budu ispravno dekodirani u Unicode, ali se kasnije opet pogrešno interpretiraju kao ANSI/UTF-8-bajtovi (ili obrnuto). U Delphi se to često događa kad se nejasno konvertuje između TBytes, RawByteString i string.
4) Trunkierung mitten in einem Multibyte-Zeichen
UTF-8 kodira specijalne znakove u 2–4 bajta. Ako čitaš u chunkovima (npr. 8 KB) i granica chunka padne usred znaka, dekoder to mora uredno puferski obraditi. Naivni pristup koji svaki chunk zasebno pretvori u string i potom ih zalijepi proizvede nevažeće sekvence. To se može manifestovati kao „sporadična“ greška, ovisno o granicama paketa, ponašanju proxyja ili HTTP-chunkingu.
5) Falsche Annahmen aus HTTP-Headern
Kod REST izvor je često Content-Type: application/json; charset=utf-8. Međutim, neki serveri ne šalju charset, neki šalju netačne informacije. Ako slepo koristiš header, to se može mijenjati ovisno o verziji backenda. Za operacije i podršku korisno je provjeriti stvarni bajt-stream i u slučaju greške ga zapisati u log.
Ein sauberer Ansatz: Stream → UTF-8-Decoder → JSON-Parser
Robustna pipeline se sastoji od tri jasne faze:
- Bytes aus dem Stream lesen (kontrolisano, po potrebi s limitom/Timeoutom u HTTP-klijentu).
- Decoding nach Unicode s eksplicitnim UTF-8 (BOM opcionalno tolerisati).
- Parsing pomoću System.JSON u objektni model ili u ciljanu ekstrakciju.
Najvažnija poluga je faza 2: Ne želiš da bilo gdje „Default Encoding“ odlučuje. U Delphi to znači: TEncoding.UTF8 eksplicitno postaviti i ne oslanjati se na implicitne konverzije.
Was „schnell“ hier konkret bedeutet
Uz System.JSON DOM nećeš „optimizirati“. Ali možeš izbjeći:
- dodatnu kopiju cijele Payload kao privremeni string, ako interno ionako trebaš samo nekoliko vrijednosti (u tom slučaju više vrijedi drugi parser; o tome kasnije),
- višestruko prekodiranje,
- i možeš kontrolisano učitavati vrlo velike Payload-ove (s ograničenjem veličine i jasnom porukom o grešci), umjesto da završiš s Out-of-Memory ili Access Violations.
Konkretan rubni slučaj: posebni znakovi oštećeni, ali samo ponekad
Jedan praktičan rubni slučaj je posebno podmukao: Payload je u osnovi važeći UTF-8 JSON, ali je čitaš u chunk-ovima i svaki chunk konvertuješ u String. Dokle god se javlja samo ASCII, ništa ne primijetiš. Čim jedan dijakritički znak (npr. umlaut) leži tačno na granici chunka, nastaju nevažeće UTF-8 sekvence. Posljedica: ili oštećeni znakovi ili parse-greška na mjestu koje nema veze s izvornim sadržajem.
Kako to prepoznati:
- Parse-greške se događaju „slučajno“ kod velikih odgovora, ne kod malih.
- Isti request ponekad uspije, ponekad ne (ovisno o chunkingu/transportu).
- Hex-dump bajtova pokazuje valjani UTF-8, ali tvoj zabilježeni String sadrži Replacement Characters (�) ili klasični Mojibake.
Rješenje nije da „više readln-aš“ ili „veće buffere“ koristiš, nego da upotrebiš decoder koji ispravno bufferuje multibajtne sekvence preko granica chunk-ova. Upravo tu TStreamReader u kombinaciji s UTF-8-encodingom može pomoći – pod uslovom da ga ispravno inicijaliziraš.
Praxisleitfaden: UTF-8 in Delphi reproduzierbar prüfen
Prije nego što diraš parser, treba ti debugging-setup koji ti prikaže stvarni niz bajtova. Za podršku i operacije to je zlata vrijedno, jer kasnije jasno možeš utvrditi isporučuje li druga strana pogrešne podatke ili ih tvoja pipeline pogrešno dekodira.
1) Prvo provjeri početne bajtove (BOM, početak JSON-a)
Ako JSON dolazi s BOM-om, na početku streama vidiš EF BB BF. Odmah nakon toga obično treba doći „{“ ili „[“. Ako u stringu već stoji „“, BOM nije tretiran kao BOM, nego je dekodiran kao tekst.
2) Logiraj sirove bajtove u Hex – ali ograničeno
Ne logiraj kompletne Payload-ove u produkciji (zaštita podataka, troškovi, volumen logova). Dokazano su korisni:
- Prefix (npr. prvih 256 ili 1024 bajta),
- Suffix (zadnjih 256 bajta),
- i hash (SHA-256) za korelaciju, ako trebaš uspoređivati Payload-ove.
Time često možeš u nekoliko minuta razjasniti probleme sa specijalnim znakovima: je li bajtovska sekvenca za „ä“ ispravna (C3 A4)? Radi li se o trunkiranju? Pojavljuje li se neočekivani 0x00 (nullbyte), npr. zbog pogrešne pretpostavke UTF-16?
3) Content-Type und Charset mitloggen
Bei HTTP/REST: Logge den Content-Type und das deklarierte charset. Wenn die Bytes klar UTF-8 sind, aber charset etwas anderes behauptet, solltest du im Client nicht blind folgen. Für JSON ist UTF-8 der De-facto-Standard. Im Zweifel: Byte-Analyse gewinnt gegen Header.
Brzi Stream-Parser mit System.JSON: Design ohne unnötige Kopien
Praktican obrazac je: čitaš iz streama u bajt-pufor, iz njega konstruišeš String tačno jednom koristeći UTF-8 i predaješ taj string JSON-parseru. To nije „streaming“ u smislu SAX-a, ali predstavlja kontrolisanu, performantu pipeline bez iznenadnih promjena enkodinga.
Važno za arhitekturu: implementiraj funkciju tako da centralno na jednom mjestu odlučuje o:
- Koje pravilo za kodiranje važi (uglavnom UTF-8, BOM opcionalno)?
- Koliko maksimalno smije biti payload (DoS-zaštita, operativna granica)?
- Kako izgledaju poruke grešaka (s kontekstom, ali bez curenja podataka)?
Strategija učitavanja: Ograničeno pufanje umjesto „StreamToString“ bez limita
Ako prihvataš JSON iz eksternih izvora (partneri, mobilni klijenti, treće strane), ograničenje veličine je obavezno. Bez limita jedan nesretan zahtjev može dovesti servis do povećanog zauzeća memorije. U praksi to znači: pri čitanju brojati ukupan broj bajtova i prekinuti pri dostizanju granice – uz jasnu iznimku koja je razumljiva u monitoringu.
Warum ich bei UTF-8 oft „ohne BOM“ erwarte, aber BOM tolerieren würde
Bei REST-Payloads kommt BOM selten vor. Bei Dateien (Exports, manuelle Bearbeitung) dagegen öfter. Für robuste Importstrecken ist es sinnvoll, BOM zu tolerieren, aber im Log sichtbar zu machen, weil es ein Hinweis auf „Datei-Welt“ statt „API-Welt“ sein kann.
Die stillen Killer: TStreamReader-Defaults und Text-Reader im Mischbetrieb
TStreamReader je praktičan, ali moraš dvije stvari čvrsto držati pod kontrolom:
- Kodiranje eksplicitno postaviti: Ne očekuj da će ga „prepoznati“.
- Razumjeti pufere: Reader unutra kešira. Ako isti stream kasnije negdje drugo čitaš, pozicija je važna. To zvuči trivijalno, ali u većim import-pipelineovima brzo postane izvor grešaka.
Posebno neugodan je miješani rad: prvo pročitati dio kao bajtove (npr. za logovanje ili magic-bytes), a potom nastaviti sa TStreamReader. Ako pri tome ne vratiš poziciju ili ne inicijalizuješ reader na ispravnom mjestu, možeš čitati od bajta 257 umjesto od 0. JSON-parser će tada prijaviti „Invalid character at position …“, iako je sama payload ispravna.
Wenn Sonderzeichen trotz UTF-8 „kaputt“ bleiben: Escaping vs. echtes Unicode
JSON može sadržavati specijalne znakove na dva načina:
- Kao stvarni UTF-8-znakovi (npr. „München“ kao bajtovi C3 BC …).
- Kao escape-sekvenca (npr. „Mu00fcnchen“).
Oboje je važeće. Za praktičnu upotrebu važno je: escape-sekvence zaobilaze mnoge probleme u transportu, ali samo prividno prikrivaju greške kodiranja. Ako vaš sistem negdje pogrešno interpretira bajtove, to je operativni rizik, ne samo kozmetički bug. Osim toga, escape-sekvence mogu zbunjivati pri logiranju/monitoringu ako timovi očekuju „čitljiv tekst“.
System.JSON vam u oba slučaja na kraju isporučuje normalne Delphi-Strings (UTF-16), pod uslovom da je put do tamo bio ispravan.
Realno procijeniti performanse: troškovi DOM-a, veliki nizovi i selektivnost
Najveći faktor za performanse često nije „ubrzati parser“, nego parsirati manje. Sa System.JSON je to teško, jer dobijete DOM. Tri tipične situacije:
- Veliki nizovi (10.000+ elemenata): Izgradnja DOM-a košta vrijeme i RAM. Ako vam trebaju samo 2 polja po elementu, parser s podrškom za streaming (SAX/Tokenizer) je često smisleniji. System.JSON nije napravljen za to.
- Pojedinačni objekti s mnogo polja: Ako vam trebaju samo neka polja, ipak možete koristiti DOM, ali izbjegavajte višestruko prelaženje. Dohvatite vrijednosti jednom i mapirajte ih u svoje strukture.
- Više velikih payloadova zaredom: U Import-Jobs ili Sync-Workerima 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“.
Zato je „brzi Stream-Parser“ s System.JSON često dobar kompromis: čitanje efikasno i ispravno, svjesno koristiti DOM, definirati granice. Ako trebate pravu streaming semantiku (npr. obrađivati elemente niza jedan po jedan bez držanja svega u memoriji), System.JSON nije odgovarajuća baza.
Robusnost u radu: obrasci grešaka i kako ih učiniti odmah vidljivim
JSON-parse-greške u logovima često nisu korisne, jer navode samo poziciju. Za operacije i podršku trebate kontekst:
- Bajtna pozicija naspram pozicije u znakovima: Kod UTF-8 to nije isto. Ako parser prijavi poziciju u znakovima, bajtna pozicija može odstupati. Za byte-dumpove je bajtna pozicija presudna.
- Isječak oko mjesta greške: Logirajte u slučaju greške mali prozor oko pozicije (npr. 40 znakova prije/poslije), ali samo ako ne sadrži osjetljive podatke. Alternativno: logirajte samo heksadecimalno.
- Korelacija: Request-ID, Endpoint, Partner-ID, Payload-Hash. Inače nećete ponovo pronaći „onu jednu grešku“.
Cilj je da nakon produkcijskog incidenta unutar nekoliko minuta možete odgovoriti: „Encoding pogrešno interpretiran“, „Payload skraćen“, „Server vraća invalid JSON“ ili „imamo problem s mapiranjem“.
Ciljano izbjegavanje zamki UTF-8: kontrolna lista
- Encoding uvijek eksplicitno: Pri čitanju iz streama i pri pisanju u logove/datoteke ne oslanjajte se na podrazumevana podešavanja.
- Bez konverzije chunkova u string: Ako čitate u chunkovima, onda prikupljajte bajtove ili koristite decoder koji buforira multibajt sekvence.
- BOM tolerantno, ali vidljivo: Prihvatiti, ali moći ga je detektovati pri debugiranju.
- Postaviti limite: Maks. veličina payloada, maks. dubina objekta/niza (ako možete to kontrolisati), timeouts u HTTP-klijentu.
Kada se trud za Stream-Parser stvarno isplati?
Ne moraš optimizirati svako JSON-mjesto. Pristup se obično isplati ako bar jedno od sljedećeg vrijedi:
- Veliki Payloads (više MB) se redovno pojavljuju ili mogu nastati.
- Longrunning-Prozesse (Service, Worker) obrađuju mnogo payloada i primjećuješ memorijske skokove ili fragmentaciju.
- Interop mit heterogenen Systemen: više partnera, različite platforme, povremeno neispravna kodiranja.
- Incident-Historie: Već su se javljali „pokvareni dijakritički znakovi“, sporadične greške pri parsiranju ili teško reproducibilni prekidi uvoza.
Ako su tvoji payloadi mali i dolaze iz kontroliranog izvora, često je dovoljan jednostavan pristup – ali čak i tada: UTF-8 eksplicitno postaviti gotovo ništa ne košta i sprječava kasnije iznenađenja.
Razgraničenje: Kada ti zaista treba streaming
System.JSON je DOM-orijentiran. Ako želiš podatke zaista obrađivati „u prolazu“, npr. veliko polje element po element, bez zadržavanja cijelog u memoriji, treba ti drugi pristup parsera (Tokenizer/SAX). To nije vrijednosni sud, nego arhitektonska odluka:
- DOM (System.JSON): praktično, dobro za tipične poslovne objekte, ali memorijski intenzivno.
- Streaming/SAX: nizak zahtjev za memoriju, pogodno za vrlo velike podatke, ali veći napor implementacije i pažljivije upravljanje greškama.
Često je smislen kompromis: uredno implementirati rukovanje streamom, enkoding i ograničenja, i tek onda odlučiti da li DOM još odgovara. U mnogim projektima to samo po sebi već značajno stabilizira rad sistema.
Zaključak: JSON in Delphi postaje pouzdan ako enkoding i streamove tretiraš kao zaseban sloj
Većina problema vezanih za „JSON in Delphi“ nema veze sa samim JSON-parserom, već s neupadljivim dijelom prije: bajtovi iz streama se pretvaraju u tekst. Ako tamo eksplicitno obradiš UTF-8, prepoznaš BOM-slučajeve, ne ignorišeš granice chunkova i postaviš jasne veličinske limite, tipične greške sa posebnim znakovima nestaju – i sporadične greške pri parsiranju postaju reproducibilne.
System.JSON ostaje pragmatski standard: nije najbrži streaming-parser, ali solidan ako kontroliraš input i svjesno prihvatiš troškove DOM-a. Ako želiš, možemo zajedno proći tvoj konkretan Import-/REST-put i identificirati mjesto na kojem enkoding ili chunking zakazuju: Kontaktiraj nas.
Za ovu temu su također važni JSON Stream Parseri. Članak ove aspekte jasno razvrstava i pokazuje na šta treba obratiti pažnju u praksi.
Razgovarati o projektu ili modernizacijskom poduhvatu s Net-Base.
Sljedeći korak
Kada se tema pretvori u stvarni projekat, arhitektura, postojeći sistem i operacije trebaju se rano sagledati zajedno.
Pružamo podršku ne samo pri pojedinačnim pitanjima, već i kada iz fragmenata izvornog koda, naslijeđenih sistema ili ideja za portal treba nastati robustan poslovni projekat.
- Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
- REST, pristup podacima, portali i Rollout se ne odgađaju kao naknadne posljedice.
- Vi rano vidite koji je put ekonomski i operativno održiv.