Net-Base Revija

13.08.2026

JSON v Delphi: Hiter Stream-Parser z System.JSON + pasti UTF-8 pri posebnih znakih

Če JSON-payloadi v Delphi prihajajo neposredno iz Streama, se iz »samo na hitro parsati« hitro razvije proizvodni problem: visoke zahteve za pomnilnik, sporadične napake pri parsiranju in poškodovani Umlauti. Ta praktični prispevek pokaže, kako z System.JSON zgradiš hiter Stream-parser...

13.08.2026

Od teme v reviji do projektne prakse

Ustrezne strani storitev in tehnični opisi k prispevku

„JSON v Delphi“ zveni kot rešen problem: System.JSON je na krovu, REST-klici vrnejo besedilo, konec zgodbe. V praksi pa se resnične napake pojavijo tam, kjer JSON ni na voljo kot udoben niz, temveč kot tok (Stream): HTTP-Response-Stream, datotečni tok, imenovana cev (Named Pipe), sporočilna čakalna vrsta ali velik BLOB iz baze podatkov. Tedaj se združijo tri stvari, ki jih v praksi pogosto podcenjujemo: obnašanje pomnilnika, kodiranje znakov (predvsem UTF-8) in robni primeri v povezavi s posebnimi znaki.

Ta prispevek pokaže čist, hiter pristop za parsanje JSON iz TStream, brez nepotrebnih kopij — in predvsem brez tipičnih pasti UTF-8, pri katerih se umlauti ali drugi posebni znaki „pokvarijo“ ali parserji občasno odpovejo z nerazložljivimi sporočili. Fokus je na posledicah za obratovanje in stabilnost vmesnikov: reproducibilno odpravljanje napak, jasne meje pristopa in kriteriji, kdaj se prizadevanje res izplača.

Zakaj se streami pri JSON-parsingu v Delphi obnašajo drugače

Dokler je JSON-dokument majhen, je pot »prebrati tok v niz, nato parsati« priročna. Pri določeni velikosti obremenitve (tipično: dolge sezname, poročila, sinhronizacijski podatki, izvozi dnevnikov) pa to postane drago:

  • Podvojitve v pomnilniku: Bajti se preberejo v medpomnilnik, pretvorijo v Unicode-niz (Delphi-String = UTF-16), parser interno ustvari dodatne strukture. To lahko začasno pomeni več kopij.
  • Pritisk na GC/heap: Veliko začasnih nizov in JSON-vrednosti poveča fragmentacijo in režijo dodeljevanja, zlasti v dolgoživečih procesih (servisi, workerji, uvozni opravila).
  • Oblika napak postane nejasna: Če so ob branju že narejene napačne predpostavke o kodiranju, parser vidi le »čudne znake« ali nepričakovane kontrolne bajte.

Pomembna je jasna ločitev: JSON je formalno Unicode, na linki pa je skoraj vedno UTF-8. Delphi pa interno dela z UTF-16. Prehod od bajtov (tok) do znakov (niz) je mesto, kjer nastanejo težave s posebnimi znaki — ne v samem JSON-u.

System.JSON: Kaj zna — in na kaj moraš paziti

System.JSON je v Delphi standard za DOM-bazirano JSON-obdelavo: dobiš objektni model (TJSONObject, TJSONArray), lahko poizveš vrednosti, iteriraš, serializiraš. To je robustno za tipične poslovne integracije, vendar ima dve posledici:

  • Ni pravi streaming-parser: Objektni model se zgradi v celoti. Morda sicer prihraniš ekstra branje v ločen niz, a DOM ostane pomnilniško zahteven.
  • Vhod parserja je ponavadi besedilo: Glede na različico Delphi in uporabljeno API hitro pristaneš spet pri nizu, vključno s konverzijo kodiranja.

Če je tvoj cilj »hiter parser toka«, v praksi običajno misliš na dve stvari: (1) brez nepotrebnih kopij in (2) čim prej fail-fast pri poškodovanih payloadih. Obe zahtevi sta dosegljivi s System.JSON, dokler kontroliraš stopnjo pretvorbe bajt → besedilo.

UTF-8-pasti pri posebnih znakih: tipični vzroki

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking je neškodljivo – dokler UTF-8 dekoder večbytna zaporedja preko mej pravilno predpomni.

Če se v izhodu umlauti (ä/ö/ü/ß) ali drugi posebni znaki prikažejo napačno (ä, – itd.), gre skoraj vedno za neusklajenost kodiranja. V okolju Delphi so ti vzroki še posebej pogosti:

1) ANSI-Fallback durch „bequeme“ Helper

Nekateri načini branja samodejno predpostavijo sistemsko ANSI-kodiranje (codepage sistema Windows), če ni podano eksplicitno kodiranje. To postane očitno šele, ko payload vsebuje kaj več kot ASCII. V testnih podatkih je to pogosto naključno „v redu“, v produkciji pa naletiš na težave pri pravih imenih, krajih ali prostem besedilu.

2) BOM-Verwirrung (Byte Order Mark)

UTF-8 se lahko začne z BOM (bytes EF BB BF). V spletnih kontekstih je BOM manj običajen, pri datotekah pa se pojavi. Nekateri bralniki BOM zaznajo in prilagodijo kodiranje, drugi ne ali le v določenih načinih. Če BOM vstopi v niz kot navaden znak, pogosto opaziš na začetku nevidni „Zero Width No-Break Space“ ali pa JSON-parser takoj zatajii pri prvem tokenu.

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

Klasična napaka „ä“ namesto „ä“ nastane, ko so UTF-8 bajti najprej pravilno dekodirani v Unicode, vendar kasneje ponovno napačno interpretirani kot ANSI/UTF-8 bajti (ali obratno). V Delphi se to pogosto zgodi, kadar je konverzija med TBytes, RawByteString in string nejasna.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF-8 kodira posebne znake v 2–4 bajtih. Če bereš v chunkih (npr. 8 KB) in meja chunka pade v sredino znaka, mora dekoder to pravilno predpomniti. Naiven pristop, ki vsak chunk posebej pretvori v niz in jih nato združi, proizvede neveljavna zaporedja. To se lahko kaže kot „sporadična“ napaka, odvisno od meja paketov, vedenja proxyja ali HTTP-chunkinga.

5) Falsche Annahmen aus HTTP-Headern

Pri REST je vir pogosto Content-Type: application/json; charset=utf-8. Nekateri strežniki pa ne pošljejo charset ali podajo napačne informacije. Če slepo uporabljaš glavo, se lahko to spreminja glede na različico backenda. Za obratovanje in podporo je koristno preveriti dejanski bajtni tok in ga v primeru napake zapisati v log.

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

Robustna pipeline je sestavljena iz treh jasnih stopenj:

  1. Bajte iz toka brati (nadzorovano, po potrebi z limitom/timeoutom v HTTP-klientu).
  2. Decodiranje v Unicode z eksplicitnim UTF-8 (BOM po potrebi tolerirati).
  3. Parsing z System.JSON v objektni model ali za ciljno ekstrakcijo.

Najpomembnejši vzvod je stopnja 2: nočeš, da kjerkoli odloča „Default Encoding“. V Delphi to pomeni: izrecno nastaviti TEncoding.UTF8 in se ne zanašati na implicitne konverzije.

Kaj „schnell“ tukaj konkretno pomeni

S System.JSON DOM-a ne boš »odstranil«. Lahko pa preprečiš:

  • dodatno kopijo celotne payload kot vmesni niz, kadar interno rabiš le nekaj vrednosti (v tem primeru je smiselnejši drug Parser; več kasneje),
  • večkratno ponovno kodiranje,
  • in lahko nadzorovano prebereš zelo velike Payloads (z omejitvijo velikosti in jasnim sporočilom o napaki), namesto da bi končal z Out-of-Memory ali Access Violations.

Konkreten robni primer: posebni znaki pokvarjeni, a le včasih

Eden izmed praktičnih robnih primerov je posebej zahrbten: Payload je sicer veljaven UTF-8-JSON, vendar ga bereš v Chunks in vsak Chunk pretvoriš v String. Dokler je prisoten le ASCII, ne opaziš nič. Ko se Umlaut točno ujame na meji Chunka, nastanejo neveljavne UTF-8-zaporedja. Rezultat: bodisi pokvarjeni znaki bodisi Parse-napaka na mestu, ki ne ustreza dejanski vsebini.

Kako to prepoznaš:

  • Parse-napake se »naključno« pojavijo pri velikih odgovorih, ne pri majhnih.
  • enak Request včasih uspe, včasih ne (odvisno od Chunkinga/Transporta).
  • Hexdump bajtov pokaže veljavno UTF-8, vendar tvoj zabeleženi String vsebuje nadomestne znake (�) ali klasični Mojibake.

Rešitev ni v tem, da »več zu readln« ali »večje Buffer« uporabiš, temveč da uporabiš dekoder, ki pravilno medpomni multibajtna zaporedja čez meje Chunkov. Ravno tu je TStreamReader v kombinaciji z UTF-8-kodiranjem koristen – če ga pravilno inicializiraš.

Praktični vodnik: kako ponovljivo preveriti UTF-8 v Delphi

Debugging-Setup mit Byte-Dump-Ausdrucken und Arbeitsmaterial, um UTF-8-Bytes und BOM in JSON-Payloads zu prüfen
Za čisto odpravljanje napak je najprej pomemben bajtni tok: BOM, trunciranje in neveljavna zaporedja so tako hitro vidni.

Preden se lotiš parserja, rabiš debugging-setup, ki ti prikaže dejansko zaporedje bajtov. Za podporo in obratovanje je to zlata vredno, saj boš kasneje lahko jasno povedal, ali nasprotna stran pošilja napačne podatke ali pa tvoja pipeline napačno dekodira.

1) Preveri prve bajte (BOM, začetek JSON-a)

Če JSON prihaja z BOM, boš na začetku toka videl EF BB BF. Takoj za tem bi običajno moral slediti „{“ ali „[“. Če se v nizu že pojavi „“, je bil BOM obdelan kot tekst in ne kot BOM.

2) Rohbytes in Hex loggen – aber begrenzt

Ne beleži popolnih Payloadov v produkciji (varstvo podatkov, stroški, obseg logov). Učinkovito se je izkazalo:

  • Prefiks (npr. prvih 256 ali 1024 bajtov),
  • Sufiks (zadnjih 256 bajtov),
  • in hash (SHA-256) za korelacijo, če moraš primerjati Payloads.

S tem lahko težave s posebnimi znaki pogosto razjasniš v nekaj minutah: je zaporedje bajtov za „ä“ pravilno (C3 A4)? Gre za trunkiranje? Ali se pojavi nepričakovani 0x00 (Nullbyte), npr. zaradi napačne predpostavke UTF-16?

3) Content-Type und Charset mitloggen

Pri HTTP/REST: zabeleži Content-Type in deklarirani charset. Če so bajti jasno UTF-8, a charset trdi drugače, odjemalcu ne sledi slepo. Za JSON je UTF-8 de-facto standard. V dvomu: analiza bajtov ima prednost pred zaglavjem.

Hiter Stream-Parser z System.JSON: zasnova brez nepotrebnih kopij

Shematska predstavitev cevovoda iz toka, UTF-8-dekodiranja, omejitve velikosti in JSON-DOM-parsanja v Delphi
Jasna cevovodna pot z omejitvijo in eksplicitnim UTF-8 jasno loči transport, dekodiranje in parsanje.

Praktičen vzorec je: bereš iz toka v medpomnilnik bajtov, iz tega sestaviš niz natanko enkrat z UTF-8 in ta niz predaš JSON-parserju. To ni »streaming« v pomenu SAX, je pa kontrolirana, zmogljiva cevovodna pot brez nepričakovanih sprememb kodiranja.

Pomembno za arhitekturo: implementiraj funkcijo tako, da na enem mestu centralno odloča:

  • Katero pravilo kodiranja velja (ponavadi UTF-8, BOM opcijsko)?
  • Kolikšna je največja dovoljena velikost payloada (zaščita pred DoS, obratovalna meja)?
  • Kako izgledajo sporočila o napaki (s kontekstom, vendar brez razkrivanja podatkov)?

Strategija branja: omejeno puferiranje namesto „StreamToString“ brez omejitve

Če sprejemaš JSON iz zunanjih virov (partnerji, mobilni odjemalci, tretje osebe), je omejitev velikosti obvezna. Brez omejitve zadostuje ena nesrečna zahteva, da storitev privede do povečane obremenitve pomnilnika. V praksi to pomeni: med branjem seštej bajte in pri dosegu meje prekini – z jasno izjemo, razumljivo v monitoringu.

Zakaj pri UTF-8 pogosto pričakujem „brez BOM“, vendar bom BOM toleriral

Pri REST-payloadih se BOM redko pojavi. Pri datotekah (eksporti, ročna obdelava) pa pogosteje. Za robustne uvozne poti je smiselno BOM tolerirati, vendar ga prikazati v dnevniku, saj lahko nakazuje »svet datotek« namesto »svet API«.

Tihi ubijalci: privzete nastavitve TStreamReader in mešana uporaba tekstovnih bralnikov

TStreamReader je priročen, vendar moraš dve stvari strogo obvladovati:

  • Kodiranje eksplicitno nastavi: Ne upaj, da ga bo »prepoznal«.
  • Sprejmi način medpomnjenja: Bralnik medpomni interno. Če isti tok kasneje drugje bereš, je položaj pomemben. Zveni trivialno, a v večjih uvoznih cevovodih hitro postane vir napak.

Še posebej neprijetna je mešana uporaba: najprej preberi del kot bajte (npr. za logiranje ali Magic-Bytes), nato nadaljuj s TStreamReader. Če se pri tem ne vrneš na pravo mesto ali ne inicializiraš bralnika na pravilni poziciji, boš bral od bajta 257 namesto od 0. JSON-parser nato javi »Invalid character at position …«, čeprav je payload sam po sebi pravilen.

Ko posebni znaki kljub UTF-8 ostanejo „poškodovani“: Escaping vs. pravi Unicode

JSON lahko vsebuje posebne znake na dva načina:

  • Kot resnično UTF-8-znake (npr. „München“ kot bajti C3 BC …).
  • Kot escape-sekvenca (npr. „Mu00fcnchen“).

Obe varianti sta veljavni. V praksi je pomembno: escape-sekvence obidejo številne transportne težave, vendar le navidezno prikrijejo napake kodiranja. Če tvoj sistem nekje napačno interpretira bajte, je to tveganje za obratovanje, ne le kozmetična napaka. Poleg tega lahko escape-sekvence pri beleženju/monitoringu zavajajo, če ekipe pričakujejo „berljivo besedilo“.

System.JSON ti v obeh primerih na koncu zagotovi običajne Delphi-nize (UTF-16), če je bila pot do tja pravilna.

Realistična ocena zmogljivosti: stroški DOM, velika polja in selektivnost

Največji vzvod za izboljšanje zmogljivosti pogosto ni „pospešiti parser“, temveč manj parsanja. Pri System.JSON je to težje, ker dobiš DOM. Tri tipične situacije:

  • Velika polja (10.000+ elementov): zgradnja DOM‑a zahteva čas in RAM. Če potrebuješ le 2 polji na element, je pogosto smiselnejši parser z zmogljivostjo pretakanja (SAX/Tokenizer). System.JSON za to ni zasnovan.
  • Posamezni objekti z veliko polji: Če potrebuješ le nekaj polj, lahko še vedno uporabiš DOM, vendar se izogibaj večkratnemu prehodu po strukturi. Vrednosti pridobi enkrat in jih mapiraj v svoje strukture.
  • Več velikih payloadov zaporedoma: Pri uvoznih opravilih ali sync‑workerjih se izplača parsanje zapakirati v jasen korak in po vsakem dokumentu sprostiti vse reference, da lahko upravitelj pomnilnika počisti. To je banalno, vendar se v servisih pogosto naredi „mimogrede“.

V praksi je „hiter stream‑parser“ skupaj s System.JSON pogosto dober kompromis: branje učinkovito in pravilno, zavestna uporaba DOM‑a, definiranje meja. Če potrebuješ pravo streaming‑semantiko (npr. obdelavo elementov polja enega za drugim brez zadrževanja vsega), System.JSON ni primerna podlaga.

Robustnost v obratovanju: vzorci napak in kako jih takoj zaznati

Napake pri JSON‑parsanju v logih pogosto niso uporabne, ker navedejo le položaj. Za obratovanje in podporo potrebuješ kontekst:

  • Položaj v bajtih vs. v znakih: Pri UTF-8 to ni enako. Če parser poroča položaj v znakih, se lahko položaj v bajtih razlikuje. Za bajtne izpise je odločilen položaj v bajtih.
  • Izrezek okoli mesta napake: V primeru napake zabeleži majhno okno okoli položaja (npr. 40 znakov pred/in za), vendar le, če ne vsebuje občutljivih podatkov. Alternativno: beleži samo v heksadecimalni obliki.
  • Korelacija: Request-ID, Endpoint, Partner-ID, Payload-Hash. Sicer tiste ene napake ne boš nikoli več našel.

Cilj je, da po produkcijskem incidentu v nekaj minutah lahko odgovoriš: „kodiranje je bilo napačno interpretirano“, „payload je bil prirezan“, „strežnik vrača invalid JSON“ ali „imamo problem z mapiranjem“.

Izogibanje pastem UTF-8: kontrolni seznam

  • Kodiranje vedno eksplicitno: Pri branju iz toka in pri pisanju v logih/datotekah se ne zanašaj na privzete nastavitve.
  • Ne pretvarjaj chunkov v nize: Če bereš v kosih (chunked), zbiri bajte ali uporabi dekoder, ki pufra multibajtna zaporedja.
  • BOM toleranten, vendar zaznaven: Sprejmi ga, vendar ga mora biti mogoče prepoznati pri razhroščevanju.
  • Postavi omejitve: Maks. velikost payloada, maks. globina objektov/polj (če jo lahko nadzoruješ), timeouti v HTTP‑klientu.
  • Ločene odgovornosti: „branje transporta“ in „parsanje JSON“ ločeno inkapsulirajte. Tako lažje razhroščite in kasneje lahko zamenjate parser.
  • Kdaj se vložek v Stream-Parser res izplača?

    Ni treba optimizirati vsakega JSON mesta. Pristop se običajno izplača, če velja vsaj ena od naslednjih točk:

    • Veliki payloadi (več MB) se pojavljajo redno ali se lahko pojavijo.
    • Dolgoživi procesi (Service, Worker) obdelujejo veliko payloadov, pri čemer opazite skoke porabe pomnilnika ali fragmentacijo.
    • Interoperabilnost z heterogenimi sistemi: več partnerjev, različne platforme, občasno napačna kodiranja.
    • Zgodovina incidentov: že so bili pokvarjeni šumniki, občasne napake pri parsiranju ali težko reproducibilne prekinitve uvoza.

    Če so tvoji payloadi majhni in prihajajo iz kontroliranega vira, pogosto zadostuje preprost pristop – vendar tudi takrat: eksplicitna nastavitev UTF-8 stane skoraj nič in prepreči poznejša presenečenja.

    Meja: kdaj res potrebuješ pravo streaming obdelavo

    System.JSON je DOM-orientiert. Če želiš podatke res obdelovati „v toku“, npr. veliko polje element za elementom, brez da bi ga držal v celoti, potrebuješ drugačen pristop parserja (Tokenizer/SAX). To ni vrednostna sodba, temveč arhitekturna odločitev:

    • DOM (System.JSON): priročno, primerno za tipične poslovne objekte, vendar pomnilniško potratno.
    • Streaming/SAX: nizka poraba pomnilnika, primerno za zelo velike podatke, vendar zahteva več implementacijskega dela in skrbnejše ravnanje z napakami.

    Smiselna kompromisna rešitev je pogosto: urejeno upravljanje toka, kodiranje in omejitve, in šele nato odločitev, ali DOM še ustreza. V mnogih projektih sam ta ukrep bistveno stabilizira obratovanje.

    Zaključek: JSON v Delphi postane zanesljiv, če kodiranje in tokove obravnavaš kot ločeno plast

    Večina težav okoli „JSON v Delphi“ ni povezana s samim JSON-parserjem, temveč z neopaznim odsekom pred njim: bajti iz toka se pretvorijo v besedilo. Če tam eksplicitno obravnavaš UTF-8, prepoznaš primere z BOM, ne ignoriraš mej chunkov in nastaviš jasne velikostne omejitve, izginejo tipične napake s posebnimi znaki – in sporadične napake pri parsiranju postanejo reproducibilne.

    System.JSON ostaja pragmatičen standard: ni najhitrejši streaming-parser, a soliden, če nadzoruješ vhod in zavestno sprejmeš stroške DOM. Če želiš, lahko skupaj pregledamo tvoj konkreten import-/REST-potek in identificiramo mesto, kjer se kodiranje ali chunking zlomi: Kontakt aufnehmen.

    Za to temo so pomembni tudi JSON Stream Parserji. Prispevek te vidike razporedi razumljivo in pokaže, na kaj je treba biti pozoren v praksi.

    Obravnava projekta ali modernizacijske pobude z Net-Base.

    naslednji korak

    Ko iz teme nastane resničen projekt, je treba arhitekturo, obstoječe sisteme in obratovanje zgodaj obravnavati skupaj.

    Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.

    • Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
    • REST, dostop do podatkov, portali in Rollout ne bodo prestavljeni v kasnejše faze.
    • Že zgodaj vidite, katera pot je ekonomsko in operativno vzdržna.

    Deli objavo

    Deli ta prispevek neposredno

    LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram pripravljamo povezavo in kratek tekst.

    E-pošta

    Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.