Net-Base Magazine

13.08.2026

JSON in Delphi: Snelle Stream-Parser met System.JSON + UTF-8-valkuilen bij speciale tekens

Wanneer JSON-payloads in Delphi rechtstreeks uit een stream komen, verandert „even snel parsen“ al snel in een productieprobleem: hoog geheugenverbruik, sporadische parsefouten en verkeerd weergegeven umlauts. Deze praktijkbijdrage laat zien hoe je met System.JSON een snelle stream-Parser opzet...

13.08.2026

Van magazinethema naar projectpraktijk

Relevante dienst- en technische pagina's bij het artikel

„JSON in Delphi“ klinkt als een opgelost probleem: System.JSON is aan boord, REST-calls leveren tekst, klaar. In de praktijk ontstaan de echte fouten echter daar waar JSON niet als een comfortabele string voorligt, maar als stream: HTTP-Response-Stream, Datei-Stream, Named Pipe, Message-Queue of een grote BLOB uit de database. Dan komen drie zaken samen die in de dagelijkse praktijk vaak worden onderschat: geheugenbeheer, tekenencoding (in het bijzonder UTF-8) en randgevallen rond speciale tekens.

Dit artikel laat een nette, snelle aanpak zien om JSON uit een TStream te parsen zonder onnodige kopieën te maken – en vooral zonder de typische UTF-8-valkuilen waarbij umlauts „kapot” gaan of parsers sporadisch met cryptische meldingen uitvallen. De focus ligt op de impact voor operatie en interface-stabiliteit: reproduceerbare debugging, duidelijke grenzen van de aanpak en criteria wanneer de inspanning echt de moeite waard is.

Waarom Streams bij JSON-parsing in Delphi anders werken

Zolang een JSON-document klein is, is de weg „Stream in String lezen, dan parse” comfortabel. Vanaf een bepaalde payload-grootte (typisch: grote lijsten, rapporten, sync-data, log-exporten) wordt dat echter duur:

  • Geheugenduplicaten: Je leest bytes in een buffer, zet ze om in een Unicode-string (Delphi-String = UTF-16), de parser maakt intern extra structuren aan. Dat kan tijdelijk meerdere kopieën betekenen.
  • GC/heap-druk: Veel tijdelijke strings en JSON-waarden verhogen fragmentatie en allocatie-overhead, vooral in langlopende processen (services, workers, import-jobs).
  • Het foutbeeld wordt onduidelijk: Als bij het inlezen al verkeerde encoding-aannames worden gemaakt, ziet de JSON-parser alleen „vreemde tekens” of onverwachte besturingsbytes.

Belangrijk is een duidelijke scheiding: JSON is formeel Unicode, op de lijn is het vrijwel altijd UTF-8. Delphi werkt intern echter met UTF-16. De overgang van bytes (Stream) naar tekens (String) is de plaats waar problemen met speciale tekens ontstaan – niet in het JSON zelf.

System.JSON: wat het goed kan – en waar je op moet letten

System.JSON is in Delphi de standaard voor DOM-gebaseerd JSON: je krijgt een objectmodel (TJSONObject, TJSONArray), kunt waarden opvragen, itereren, serialiseren. Dat is robuust voor typische business-integraties, maar heeft twee consequenties:

  • Het is geen echte streaming-parser: Het objectmodel wordt volledig opgebouwd. Je bespaart mogelijk het inlezen in een extra string, maar het DOM blijft geheugenintensief.
  • De parser-input is in de regel tekst: Afhankelijk van de Delphi-versie en gebruikte API belandt men snel weer bij de string, inclusief encoding-conversie.

Als je doel „snelle Stream-Parser” is, bedoel je in de praktijk meestal een van twee dingen: (1) geen onnodige kopieën en (2) zo vroeg mogelijk fail-fast bij defecte payloads. Beiden zijn met System.JSON te bereiken, zolang je de byte-naar-tekst-laag onder controle houdt.

UTF-8-valkuilen bij speciale tekens: de typische oorzaken

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking is onschadelijk – zolang de UTF-8-decoder multibyte-sequenties over grenzen buffert.

Wenn Umlaute (ä/ö/ü/ß) oder andere Sonderzeichen im Ergebnis falsch aussehen (ä, – etc.), ist das fast immer ein Encoding-Mismatch. Im Delphi-Umfeld sind diese Ursachen besonders häufig:

1) ANSI-Fallback durch „bequeme“ Helper

Sommige leespaden nemen stilzwijgend het systeem-ANSI-encoding (Codepage des Windows-Systems) aan wanneer er geen expliciete codering wordt meegegeven. Dat valt pas op als een payload niet alleen ASCII bevat. In testgegevens is dat vaak toevallig ‘ok’, in productie gaat het mis bij echte namen, locaties, vrije tekst.

2) BOM-Verwirrung (Byte Order Mark)

UTF-8 kan met een BOM beginnen (bytes EF BB BF). In webcontext is een BOM vrij ongebruikelijk, bij bestanden komt het voor. Sommige readers herkennen de BOM en passen de codering aan, andere niet of alleen in bepaalde modi. Als een BOM als gewoon teken in de string terechtkomt, zie je vaak een onzichtbare „Zero Width No-Break Space“ aan het begin of faalt de JSON-parser direct bij het eerste token.

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

Het klassieke foutbeeld „ä“ statt „ä“ ontstaat wanneer UTF-8-bytes eerst correct naar Unicode zijn gedecodeerd, maar later opnieuw als ANSI/UTF-8-bytes verkeerd worden geïnterpreteerd (of omgekeerd). In Delphi gebeurt dat graag wanneer onduidelijk wordt geconverteerd tussen TBytes, RawByteString en string.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF-8 codeert speciale tekens in 2–4 bytes. Wanneer je in chunks leest (bijv. 8 KB) en de chunk-grens midden in een teken valt, moet de decoder dat netjes bufferen. Een naïeve aanpak die elke chunk afzonderlijk naar een string omzet en aan elkaar plakt, produceert ongeldige sequenties. Dat kan als een „sporadische“ fout lijken, afhankelijk van pakketgrenzen, proxy-gedrag of HTTP-chunking.

5) Falsche Annahmen aus HTTP-Headern

Bij REST is de bron vaak Content-Type: application/json; charset=utf-8. Sommige servers leveren echter geen charset, andere leveren onjuiste informatie. Als je blind op de header vertrouwt, kan dat per backend-versie wisselen. Voor operatie en support is het nuttig om de daadwerkelijke bytestroom te controleren en bij fouten te loggen.

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

De robuuste pijplijn bestaat uit drie duidelijke stappen:

  1. Bytes uit de stream lezen (gecontroleerd, eventueel met limiet/timeout in de HTTP-client).
  2. Decoderen naar Unicode met expliciet UTF-8 (BOM optioneel tolereren).
  3. Parsen met System.JSON naar een objectmodel of naar gerichte extractie.

De belangrijkste hefboom is stap 2: je wilt niet dat ergens „Default Encoding“ beslist. In Delphi betekent dat: TEncoding.UTF8 expliciet instellen en niet vertrouwen op impliciete conversies.

Was „schnell“ hier konkret bedeutet

Met System.JSON ga je het DOM niet ‚wegoptimaliseren‘. Maar je kunt voorkomen:

  • een extra kopie van de volledige Payload als tussentijdse string, als je intern toch maar een paar waarden nodig hebt (in dat geval is eerder een andere parser zinvol; daarover later),
  • meerdere keren hercoderen,
  • en je kunt zeer grote Payloads gecontroleerd inlezen (met groottebeperking en heldere foutmelding), in plaats van te eindigen met Out-of-Memory of Access Violations.

De concrete randgeval: speciale tekens beschadigd, maar alleen soms

Een randgeval uit de praktijk is bijzonder verraderlijk: de Payload is in wezen geldig UTF-8-JSON, maar je leest deze in chunks en converteert per chunk naar een string. Zolang alleen ASCII voorkomt, merk je er niets van. Zodra een Umlaut precies op een chunk-grens valt, ontstaan er ongeldige UTF-8-sequenties. Resultaat: of beschadigde tekens of een parse-fout op een plaats die niet bij de eigenlijke inhoud past.

Waaraan je dat herkent:

  • Parse-fouten treden ‚toevallig‘ op bij grote antwoorden, niet bij kleine.
  • Dezelfde Request werkt de ene keer wel, de andere keer niet (afhankelijk van Chunking/Transport).
  • Een hexdump van de bytes toont geldig UTF-8, maar je gelogde string bevat Replacement Characters (�) of klassieke Mojibake.

De oplossing is niet om ‚meer te readln‘ of ‚grotere Buffer‘ te gebruiken, maar een decoder te gebruiken die multibyte-sequenties over chunk-grenzen heen correct buffert. Dat is precies het punt waarop TStreamReader in combinatie met een UTF-8-Encoding nuttig kan zijn — als je deze correct initialiseert.

Praktijkhandleiding: UTF-8 in Delphi reproduceerbaar controleren

Debugging-opstelling met byte-dumpafdrukken en werkmateriaal om UTF-8-bytes en BOM in JSON-payloads te controleren
Voor degelijk debuggen telt eerst de bytestroom: BOM, truncatie en ongeldige sequenties zijn zo snel zichtbaar.

Voordat je aan de parser sleutelt, heb je een debugging-opstelling nodig die je de daadwerkelijke bytevolgorde zichtbaar maakt. Voor support en operatie is dat goud waard, omdat je later duidelijk kunt aantonen of de tegenpartij verkeerde data levert of jouw pijplijn verkeerd decodeert.

1) De eerste bytes controleren (BOM, JSON-start)

Als het JSON met BOM komt, zie je aan het begin van de stream EF BB BF. Direct daarna zou typisch „{“ of „[“ moeten komen. Als er al „“ in de string staat, is de BOM niet als BOM behandeld maar als tekst gedecodeerd.

2) Rauwe bytes in hex loggen — maar beperkt

Log niet complete Payloads in productie (privacy, kosten, logvolume). Bewezen werkwijzen zijn:

  • Prefix (bijv. eerste 256 of 1024 Bytes),
  • Suffix (laatste 256 Bytes),
  • en een Hash (SHA-256) voor correlatie, als je Payloads moet vergelijken.

Daarmee kun je problemen met speciale tekens vaak binnen enkele minuten classificeren: is de bytevolgorde voor „ä“ correct (C3 A4)? Is er truncatie opgetreden? Verschijnt er een onverwachte 0x00 (nulbyte), bijvoorbeeld door een verkeerde aanname van UTF-16?

3) Content-Type und Charset mitloggen

Bij HTTP/REST: log de Content-Type en het opgegeven charset. Als de bytes duidelijk UTF-8 zijn, maar het charset iets anders claimt, moet je in de client niet blindelings volgen. Voor JSON is UTF-8 de facto-standaard. Bij twijfel: byte-analyse wint van headers.

Snelle Stream-Parser met System.JSON: Design zonder onnodige kopieën

Schematische weergave van een pijplijn bestaande uit Stream, UTF-8-decoding, groottebegrenzing en JSON-DOM-parsing in Delphi
Een duidelijke pijplijn met limiet en expliciet UTF-8 scheidt transport, decodering en parsing netjes.

Een praktijkgeschikt patroon is: je leest uit de Stream in een bytebuffer, bouwt daaruit een string precies één keer met UTF-8, en geeft die string door aan de JSON-Parser. Dat is niet „streaming“ in de zin van SAX, maar het is een gecontroleerde, performante pijplijn zonder onverwachte encoding-wisselingen.

Belangrijk voor de architectuur: bouw de functie zo dat op één plek centraal wordt besloten:

  • Welke encoderingregel geldt (meestal UTF-8, BOM optioneel)?
  • Hoe groot mag de Payload maximaal zijn (DoS-bescherming, operationele grens)?
  • Hoe zien foutmeldingen eruit (met context, maar zonder datalek)?

Inleesstrategie: begrensd bufferen in plaats van „StreamToString“ zonder limiet

Als je JSON van externe bronnen accepteert (partners, mobiele Clients, derden), is een groottebegrenzing verplicht. Zonder limiet kan één ongelukkige Request een dienst in Memory Pressure brengen. Praktisch betekent dit: tijdens het lezen de som van de bytes tellen en bij het bereiken van een grens afbreken – met een duidelijke Exception die in de monitoring begrijpelijk is.

Waarom ik bij UTF-8 vaak „zonder BOM“ verwacht, maar BOM zou tolereren

Bij REST-Payloads komt BOM zelden voor. Bij bestanden (Exports, handmatige bewerking) daarentegen vaker. Voor robuuste importpaden is het zinvol BOM te tolereren, maar zichtbaar in de logs te maken, omdat het kan wijzen op de „bestandswereld“ in plaats van de „API-wereld“.

De stille killers: TStreamReader-Defaults en Text-Reader in gemengd gebruik

TStreamReader is handig, maar je moet twee zaken goed onder controle hebben:

  • Encodering expliciet instellen: Niet hopen dat hij het „herkent“.
  • Bufferen begrijpen: De Reader bufferet intern. Als je dezelfde Stream later nog eens ergens anders leest, is de positie relevant. Dat klinkt triviaal, maar wordt in grotere importpijplijnen snel een foutbron.

Gemengd gebruik is bijzonder vervelend: eerst een deel als bytes lezen (bijv. voor Logging of Magic-Bytes), daarna verder met TStreamReader. Als je daarbij niet correct terugspoelt of de Reader-initialisatie op de juiste positie doet, lees je vanaf Byte 257 in plaats van vanaf 0. De JSON-Parser meldt dan „Invalid character at position …“, terwijl de Payload op zich correct is.

Wanneer speciale tekens ondanks UTF-8 „kapot“ blijven: Escaping vs. echt Unicode

JSON kan speciale tekens op twee manieren bevatten:

  • Als echte UTF-8-tekens (bijv. „München“ als Bytes C3 BC …).
  • Als escape-sequentie (bijvoorbeeld „Mu00fcnchen“).

Beide zijn geldig. Voor de praktijk is belangrijk: escape-sequenties omzeilen veel transportproblemen, maar ze maskeren encoderingfouten slechts schijnbaar. Als jouw systeem ergens bytes verkeerd interpreteert, is dat een operationeel risico, niet alleen een cosmetische bug. Bovendien kunnen escape-sequenties bij logging/monitoring verwarring veroorzaken als teams verwachten “leesbare tekst” te zien.

System.JSON liefert dir in beiden Fällen am Ende normale Delphi-Strings (UTF-16), sofern der Weg bis dahin korrekt war.

Performance realistisch einordnen: DOM-Kosten, große Arrays und Selektivität

De grootste performancehefboom is vaak niet “de parser sneller maken”, maar minder parsen. Met System.JSON is dat lastig, omdat je het DOM krijgt. Drie typische situaties:

  • Grote arrays (10.000+ Elemente): Het opbouwen van het DOM kost tijd en RAM. Als je maar 2 velden per element nodig hebt, is een parser die streaming ondersteunt (SAX/Tokenizer) vaak verstandiger. System.JSON is daar niet voor ontworpen.
  • Einzelobjekte mit vielen Feldern: Als je slechts enkele velden nodig hebt, kun je nog steeds het DOM gebruiken, maar vermijd meerdere keren traverseren. Haal waarden één keer op en map ze naar je structuren.
  • Mehrere große Payloads hintereinander: Bij import-jobs of sync-workers verdient het de voorkeur het parsen in een duidelijke stap te kapselen en na elk document alle referenties vrij te geven, zodat de memory-manager kan opruimen. Dat is banaal, maar in services wordt het graag “er even bij gedaan”.

Een “snelle stream-parser” met System.JSON is daarom vaak een goede compromis: efficiënt en correct inlezen, het DOM bewust gebruiken, grenzen definiëren. Als je echte streaming-semantiek nodig hebt (bijvoorbeeld array-elementen één voor één verwerken zonder alles vast te houden), is System.JSON niet de juiste basis.

Robustheit im Betrieb: Fehlerbilder und wie du sie sofort greifbar machst

JSON-parsefouten in logs zijn vaak niet behulpzaam, omdat ze slechts een positie noemen. Voor productie en support heb je context nodig:

  • Byte-Position vs. Zeichen-Position: Bij UTF-8 zijn die niet identiek. Als de parser een tekenpositie rapporteert, kan de bytepositie afwijken. Voor byte-dumps is de bytepositie doorslaggevend.
  • Snippet um die Fehlerstelle: Log bij fouten een klein venster rond de positie (bijvoorbeeld 40 tekens ervoor/erna), maar alleen als er geen gevoelige gegevens in staan. Als alternatief: log alleen hexadecimaal.
  • Korrelation: Request-ID, Endpoint, Partner-ID, Payload-Hash. Anders vind je “die ene fout” nooit meer terug.

Het doel is dat je na een productie-incident binnen enkele minuten kunt beantwoorden: “Encoding verkeerd geïnterpreteerd”, “Payload afgekapt”, “Server levert invalid JSON” of “wij hebben een mapping-probleem”.

UTF-8-Fallstricke gezielt vermeiden: Checkliste

  • Encoding immer explizit: Bij het lezen uit een stream en bij het schrijven naar logs/bestanden niet op defaults vertrouwen.
  • Keine Chunk-zu-String-Konvertierung: Als je in chunks leest, verzamel dan bytes of gebruik een decoder die multibyte-sequenties buffert.
  • BOM tolerant, aber sichtbar: BOM tolereren, maar in debugging herkenbaar maken.
  • Limits setzen: Max. payload-grootte, max. object-/array-diepte (als je die kunt controleren), timeouts in de HTTP-client.
  • Gescheiden verantwoordelijkheden: „Transport lezen“ en „JSON parsen“ afzonderlijk kapselen. Zo debug je sneller en kun je later een parser uitwisselen.
  • Wanneer loont de inspanning voor een Stream-Parser echt?

    Je hoeft niet elke JSON-locatie te optimaliseren. De aanpak loont zich doorgaans wanneer minstens één van de volgende punten van toepassing is:

    • Grote Payloads (enkele MB) komen regelmatig voor of kunnen zich voordoen.
    • Longrunning-Prozesse (Service, Worker) verwerken veel payloads en je ziet geheugenpieken of fragmentatie.
    • Interop met heterogene systemen: meerdere partners, verschillende platformen, soms foutieve encodings.
    • Incident-historie: er zijn al „kaputte Umlaute“, sporadische parse-fouten of moeilijk reproduceerbare importafbrekingen geweest.

    Als je payloads klein zijn en uit een gecontroleerde bron komen, volstaat vaak een eenvoudige aanpak — maar ook dan: UTF-8 expliciet instellen kost bijna niets en voorkomt latere verrassingen.

    Afgrenzing: wanneer je echt Streaming nodig hebt

    System.JSON is DOM-georiënteerd. Als je data echt in één doorloop wilt verwerken, bijvoorbeeld een grote array element voor element zonder die volledig in geheugen te houden, heb je een andere parser-aanpak nodig (Tokenizer/SAX). Dat is geen waardeoordeel maar een architectuurkeuze:

    • DOM (System.JSON): comfortabel, goed voor typische business-objecten, maar geheugenintensief.
    • Streaming/SAX: lager geheugengebruik, geschikt voor zeer grote datasets, maar meer implementatie-inspanningen en zorgvuldiger foutafhandeling.

    Een zinvol compromis is vaak: stream-handling, encoding en limieten netjes opzetten, en pas dan beslissen of DOM nog past. In veel projecten stabiliseert dat op zichzelf al de operatie aanzienlijk.

    Conclusie: JSON in Delphi wordt betrouwbaar als je Encoding en Streams als een eigen laag behandelt

    De meeste problemen rond „JSON in Delphi“ hebben niet met de JSON-parser zelf te maken, maar met het onopvallende traject daarvoor: bytes uit een stream worden naar tekst omgezet. Als je daar UTF-8 expliciet behandelt, BOM-gevallen herkent, chunk-grenzen niet negeert en duidelijke grootte-limieten instelt, verdwijnen de typische speciale-tekenfouten — en worden sporadische parse-problemen reproduceerbaar.

    System.JSON blijft daarbij een pragmatische standaard: niet de snelste streaming-parser, maar solide als je de input controleert en de DOM-kosten bewust accepteert. Als je wilt, kunnen we jouw concrete import-/REST-pad samen doornemen en het punt identificeren waar Encoding of Chunking faalt: contact opnemen.

    Voor dit onderwerp zijn ook JSON Stream Parser van belang. Het artikel plaatst deze aspecten begrijpelijk en laat zien waar het in de praktijk om gaat.

    Project of moderniseringsproject met Net-Base bespreken.

    volgende stap

    Wanneer het onderwerp een concreet project wordt, moeten architectuur, bestaande omgeving en exploitatie vroegtijdig samen worden bekeken.

    We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.

    • Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
    • REST, toegang tot gegevens, portalen en rollout worden niet naar latere fasen verschoven.
    • U ziet vroeg welke weg economisch en operationeel levensvatbaar is.

    Bericht delen

    Dit bericht direct delen

    LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

    E-mail

    Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.