Net-Base Magasin

13.08.2026

JSON i Delphi: Rask streamparser med System.JSON + UTF-8-fallgruver ved spesialtegn

Når JSON-payloads i Delphi kommer direkte fra en stream, blir «bare å parse» raskt et produksjonsproblem: høyt minneforbruk, sporadiske parse-feil og ødelagte umlaut-tegn. Denne praksisartikkelen viser hvordan du bygger en rask stream-parser med System.JSON.

13.08.2026

Fra magasinetema til prosjektpraksis

Egnede tjeneste- og tekniske sider for innlegget

„JSON i Delphi“ høres ut som et løst problem: System.JSON er om bord, REST-calls leverer tekst, ferdig. I praksis oppstår de reelle feilene der JSON ikke foreligger som en komfortabel streng, men som en stream: HTTP-response-stream, filstream, named pipe, message queue eller en stor BLOB fra databasen. Da møtes tre forhold som ofte undervurderes i praksis: minnehåndtering, tegnkoding (spesielt UTF-8) og grense­tilfeller rundt spesialtegn.

Denne artikkelen viser en ren, rask tilnærming for å parse JSON fra en TStream uten å lage unødvendige kopier — og først og fremst uten de typiske UTF-8-fellene der Umlaute blir «øde lagt» eller parseren sporadisk feiler med kryptiske meldinger. Fokuset ligger på konsekvenser for drift og grensesnittstabilitet: reproducerbar feilsøking, klare begrensninger i tilnærmingen og kriterier for når innsatsen faktisk lønner seg.

Hvorfor streams oppfører seg annerledes ved JSON-parsing i Delphi

Så lenge et JSON-dokument er lite, er veien «les stream til streng, så parse» praktisk. Ved en viss payload-størrelse (typisk: store lister, rapporter, sync-data, logg-eksporter) blir det imidlertid kostbart:

  • Minne-duplikater: Du leser bytes inn i en buffer, konverterer til en Unicode-streng (Delphi-String = UTF-16), og parseren oppretter interne strukturer. Det kan midlertidig gi flere kopier.
  • Økt GC-/heap-press: Mange midlertidige strenger og JSON-verdier øker fragmentering og allokerings­overhead, spesielt i langkjørende prosesser (services, workers, import-jobber).
  • Feilbildet blir uklart: Hvis feil antagelser om encoding skjer allerede ved innlesning, ser JSON-parseren kun «rare tegn» eller uventede styringsbytes.

Viktig er en klar separasjon: JSON er formelt Unicode, men på forbindelsen er det nesten alltid UTF-8. Delphi arbeider internt med UTF-16. Overgangen fra bytes (stream) til tegn (streng) er punktet der problemer med spesialtegn oppstår — ikke i JSON-formatet selv.

System.JSON: Hva det gjør bra — og hva du må passe på

System.JSON er i Delphi standarden for DOM-basert JSON: Du får et objektmodell (TJSONObject, TJSONArray), kan hente verdier, iterere og serialisere. Dette er robust for typiske forretningsintegrasjoner, men har to konsekvenser:

  • Det er ikke en ekte streaming-parser: Objektmodellen bygges opp i sin helhet. Du sparer kanskje innlesing til en ekstra streng, men DOM-en forblir minne­tung.
  • Parserens inndata er som regel tekst: Avhengig av Delphi-versjon og valgt API ender man raskt opp igjen med en streng, inkludert encoding-konvertering.

Når målet ditt er en «rask stream-parser», mener du i praksis vanligvis ett av to ting: (1) ingen unødvendige kopier og (2) så tidlig som mulig fail-fast ved defekt payload. Begge deler er mulig med System.JSON, så lenge du kontrollerer trinnet fra byte til tekst.

UTF-8-fallgruver for spesialtegn: de typiske årsakene

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking ist harmlos – solange der UTF-8-Decoder Mehrbyte-Sequenzen über Grenzen puffert.

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

Einige Lesewege nehmen stillschweigend das System-ANSI-Encoding (Codepage des Windows-Systems) an, wenn keine explizite Kodierung übergeben wird. Das fällt erst auf, wenn ein Payload nicht nur ASCII enthält. In Testdaten ist das oft zufällig „okay“, in Produktion knallt es bei echten Namen, Orten, Freitexten.

2) BOM-Verwirrung (Byte Order Mark)

UTF-8 kann mit BOM beginnen (Bytes EF BB BF). Im Web-Kontext ist BOM eher unüblich, bei Dateien kommt es vor. Manche Reader erkennen BOM und passen das Encoding an, andere nicht oder nur in bestimmten Modi. Wenn ein BOM als normales Zeichen in den String rutscht, siehst du oft ein unsichtbares „Zero Width No-Break Space“ am Anfang oder der JSON-Parser scheitert direkt am ersten Token.

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

Das klassische Fehlerbild „ä“ statt „ä“ entsteht, wenn UTF-8-Bytes zuerst korrekt nach Unicode decodiert wurden, aber später nochmals als ANSI/UTF-8-Bytes fehlinterpretiert werden (oder umgekehrt). In Delphi passiert das gern, wenn zwischen TBytes, RawByteString und string unklar konvertiert wird.

4) Trunkierung mitten in einem Multibyte-Zeichen

UTF-8 kodiert Sonderzeichen in 2–4 Bytes. Wenn du in Chunks liest (z. B. 8 KB) und die Chunk-Grenze mitten im Zeichen liegt, muss der Decoder das sauber puffern. Ein naiver Ansatz, der jeden Chunk separat in String umwandelt und zusammenklebt, produziert ungültige Sequenzen. Das kann als „sporadischer“ Fehler wirken, abhängig von Paketgrenzen, Proxy-Verhalten oder HTTP-Chunking.

5) Falsche Annahmen aus HTTP-Headern

Bei REST ist die Quelle oft Content-Type: application/json; charset=utf-8. Manche Server liefern aber kein charset, manche liefern falsche Angaben. Wenn du blind den Header nutzt, kann das je nach Backend-Version wechseln. Für Betrieb und Support ist es hilfreich, den tatsächlichen Byte-Stream zu prüfen und im Fehlerfall zu loggen.

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

Die robuste Pipeline besteht aus drei klaren Stufen:

  1. Bytes aus dem Stream lesen (kontrolliert, ggf. mit Limit/Timeout im HTTP-Client).
  2. Decoding nach Unicode mit explizitem UTF-8 (BOM optional tolerieren).
  3. Parsing mit System.JSON in ein Objektmodell oder in gezielte Extraktion.

Der wichtigste Hebel ist Stufe 2: Du willst nicht, dass irgendwo „Default Encoding“ entscheidet. In Delphi bedeutet das: TEncoding.UTF8 explizit setzen und nicht auf implizite Konvertierungen vertrauen.

Was „schnell“ hier konkret bedeutet

Med System.JSON vil du ikke „optimere bort“ DOM-en. Men du kan unngå:

  • en ekstra kopi av hele payloaden som mellomstreng, hvis du internt uansett bare trenger få verdier (da lønner det seg heller med en annen parser; mer om det senere),
  • flere omkodinger,
  • og du kan lese svært store payloads kontrollert (med størrelsesgrense og klar feilmelding), i stedet for å ende i Out-of-Memory eller Access Violations.

Et konkret tilfelle: spesialtegn ødelagt, men bare noen ganger

Et spesielt vrient tilfelle fra praksis er dette: Payloaden er i utgangspunktet gyldig UTF-8-JSON, men du leser den i chunks og konverterer hver chunk til en streng. Så lenge det bare er ASCII, merker du ingenting. Så snart en bokstav med utslag (f.eks. en umlaut) ligger nøyaktig på en chunk-grense, oppstår ugyldige UTF-8-sekvenser. Resultatet: enten ødelagte tegn eller en parse-feil på et sted som ikke stemmer med innholdet.

Slik kjenner du det igjen:

  • Parse-feil opptrer «tilfeldig» ved store svar, ikke ved små.
  • Samme request fungerer noen ganger, andre ganger ikke (avhengig av chunking/transport).
  • En hexdump av bytene viser gyldig UTF-8, men din loggede streng inneholder Replacement Characters (�) eller klassisk mojibake.

Løsningen er ikke å «readln mer» eller bruke «større buffere», men å benytte en decoder som korrekt bufrer multibyte-sekvenser over chunk-grenser. Det er nettopp der TStreamReader kombinert med en UTF-8-encoding kan være nyttig – forutsatt at du initialiserer den korrekt.

Praktisk veiledning: Reproduserbar kontroll av UTF-8 i Delphi

Debugging-Setup mit Byte-Dump-Ausdrucken und Arbeitsmaterial, um UTF-8-Bytes und BOM in JSON-Payloads zu prüfen
For ryddig debugging er byte-strømmen det viktigste: BOM, trunkering og ugyldige sekvenser blir da raskt synlige.

Før du endrer på parseren, trenger du et debugging-oppsett som viser den faktiske bytefølgen. For support og drift er dette gull verdt, fordi du senere klart kan si om motparten leverer feil data eller om pipelinen din dekoder feil.

1) Sjekk de første bytene (BOM, JSON-start)

Hvis JSON-en kommer med BOM, ser du EF BB BF ved begynnelsen av strømmen. Like etter bør typisk „{“ eller „[“ komme. Hvis „“ allerede står i strengen, ble BOM ikke behandlet som BOM, men dekodet som tekst.

2) Logg råbytes i Hex – men begrenset

Ikke logg komplette payloads i produksjon (personvern, kostnader, loggvolum). Følgende har vist seg nyttig:

  • Prefix (f.eks. de første 256 eller 1024 bytene),
  • Suffix (de siste 256 bytene),
  • og en hash (SHA-256) for korrelasjon når du må sammenligne payloads.

Med dette kan du ofte plassere problemer med spesialtegn på minutter: Er bytefølgen for „ä“ korrekt (C3 A4)? Foreligger det trunkering? Dukker et uventet 0x00 (nullbyte) opp, f.eks. på grunn av feil antakelse om UTF-16?

3) Content-Type und Charset mitloggen

Ved HTTP/REST: logg den Content-Type og den deklarerte charset. Hvis byte-sekvensen tydelig er UTF-8, men charset hevder noe annet, bør du ikke følge det blindt i klienten. For JSON er UTF-8 de facto-standarden. Når det er tvil: byte-analyse vinner over headeren.

Rask strømparser med System.JSON: design uten unødvendige kopier

Skjematisk framstilling av en pipeline bestående av stream, UTF-8-dekoding, størrelsesbegrensning og JSON-DOM-parsing i Delphi
En tydelig pipeline med grense og eksplisitt UTF-8 skiller transport, dekoding og parsing klart.

Et praktisk mønster er: du leser fra streamen inn i en byte-buffer, bygger derfra en String nøyaktig én gang med UTF-8, og gir denne Stringen til JSON-parseren. Dette er ikke «streaming» i SAX-forstand, men det er en kontrollert, ytelseseffektiv pipeline uten overraskende kodingsbytter.

Viktig for arkitekturen: bygg funksjonen slik at den sentralt avgjør på ett sted:

  • Hvilken encoding-regel gjelder (som regel UTF-8, BOM valgfritt)?
  • Hvor stor kan nyttelasten være maksimalt (DoS-beskyttelse, driftsgrense)?
  • Hvordan ser feilmeldinger ut (med kontekst, men uten datalekkasje)?

Innlesningsstrategi: Begrenset buffering i stedet for «StreamToString» uten grense

Hvis du aksepterer JSON fra eksterne kilder (partnere, mobile klienter, tredjepartsleverandører), er en størrelsesgrense obligatorisk. Uten grense er det nok med én uheldig forespørsel for å bringe en tjeneste i memory pressure. Praktisk betyr det: tell summen av bytes under lesing og avbryt ved en grense – med et tydelig unntak som er forståelig i overvåkingen.

Hvorfor jeg ofte forventer UTF-8 «uten BOM», men vil tolerere BOM

Ved REST-payloads forekommer BOM sjelden. Ved filer (eksport, manuell redigering) forekommer det derimot oftere. For robuste importløp er det fornuftig å tolerere BOM, men synliggjøre det i loggen, fordi det kan være en indikasjon på «filverden» i stedet for «API-verden».

De stille killerne: TStreamReader-standardinnstillinger og tekstlesere i blandet bruk

TStreamReader er praktisk, men du må ha to ting godt under kontroll:

  • Sett encoding eksplisitt: Ikke forvent at den «gjenkjenner» den.
  • Forstå buffering: Readeren bufferer internt. Hvis du leser samme stream et annet sted senere, er posisjonen relevant. Det høres trivielt ut, men blir raskt en feilkilde i større importpipelines.

Særlig problematisk er blandet bruk: først lese en del som bytes (f.eks. for logging eller magic-bytes), deretter fortsette med TStreamReader. Hvis du ikke spoler tilbake korrekt eller initialiserer readeren ved riktig posisjon, leser du fra byte 257 i stedet for fra 0. JSON-parseren rapporterer da «Invalid character at position …», selv om payloaden i seg selv er korrekt.

Når spesialtegn forblir «ødelagt» til tross for UTF-8: escaping vs. ekte Unicode

JSON kan inneholde spesialtegn på to måter:

  • Som ekte UTF-8-tegn (f.eks. «München» som bytes C3 BC …).
  • Som escape-sekvens (f.eks. „Mu00fcnchen“).

Begge deler er gyldig. I praksis er det viktig: Escape-sekvenser unngår mange transportproblemer, men de skjuler bare tilsynelatende enkodingsfeil. Hvis systemet ditt et sted tolker bytes feil, er det en driftsrisiko, ikke bare en kosmetisk bug. I tillegg kan escape-sekvenser forvirre ved logging/monitorering hvis teamene forventer å se «lesbar tekst».

System.JSON leverer i begge tilfeller til slutt normale Delphi-strenger (UTF-16), forutsatt at veien dit var korrekt.

Vurder ytelse realistisk: DOM-kostnader, store Arrays og selektivitet

Den største ytelsesfaktoren er ofte ikke „gjøre parseren raskere“, men å parse mindre. Med System.JSON er det vanskelig, siden du får DOM. Tre typiske situasjoner:

  • Store Arrays (10.000+ elementer): Oppbygging av DOM koster tid og RAM. Hvis du bare trenger 2 felt per element, er en streaming-vennlig parser (SAX/Tokenizer) ofte mer hensiktsmessig. System.JSON er ikke laget for det.
  • Enkeltobjekter med mange felt: Hvis du bare trenger få felt, kan du likevel bruke DOM, men unngå å traversere flere ganger. Hent verdiene én gang og map dem inn i strukturene dine.
  • Flere store payloads på rad: I import-jobber eller Sync-Worker lønner det seg å kapsle parsing i et eget steg og etter hvert dokument frigjøre alle referanser, slik at minnehåndtereren kan rydde opp. Det er banalt, men i tjenester gjøres det ofte „ved siden av“.

En „rask stream-parser“ med System.JSON er derfor ofte et godt kompromiss: Lesing effektivt og korrekt, bruk DOM bevisst, definer grenser. Hvis du trenger ekte streaming-semantikk (f.eks. behandle array-elementer ett og ett uten å holde alt i minnet), er ikke System.JSON riktig grunnlag.

Robusthet i drift: Feilmønstre og hvordan du gjør dem umiddelbart håndgripelige

JSON-parse-feil i logger er ofte ikke hjelpsomme, fordi de bare oppgir en posisjon. For drift og support trenger du kontekst:

  • Byte-posisjon vs tegn-posisjon: For UTF-8 er dette ikke identisk. Hvis parseren rapporterer en tegnposisjon, kan byteposisjonen avvike. For byte-dumps er byteposisjonen avgjørende.
  • Utdrag rundt feilstedet: Logg ved feil et lite vindu rundt posisjonen (f.eks. 40 tegn før/etter), men bare hvis det ikke inneholder sensitive opplysninger. Alternativt: logg kun i heksadesimal.
  • Korrelasjon: Request-ID, Endpoint, Partner-ID, Payload-Hash. Ellers finner du «den ene feilen» aldri igjen.

Målet er at du etter en produksjonsincident innen få minutter kan svare: «Enkodning tolket feil», «Payload avkortet», «Server leverer ugyldig JSON» eller «vi har et mapping-problem».

Unngå UTF-8-fallgruver målrettet: Sjekkliste

  • Angi enkoding alltid eksplisitt: Ved lesing fra stream og ved skriving til logger/filer, ikke stol på standardinnstillingene.
  • Ingen chunk-til-string-konvertering: Hvis du leser i chunks, samle bytes eller bruk en dekoder som buffrer multibyte-sekvenser.
  • BOM-tolerant, men synlig: Aksepter, men kunne oppdage den i debugging.
  • Sett grenser: Maks. payload-størrelse, maks. objekt-/array-dybde (dersom du kan kontrollere det), timeouts i HTTP-klienten.
  • Adskilte ansvarsområder: „lese transport“ og „parse JSON“ kapsle hver for seg. Slik feilsøker man raskere og kan senere bytte ut en parser.
  • Når lønner det seg egentlig å bruke en stream-parser?

    Du trenger ikke å optimalisere alle JSON-steder. Tilnærmingen lønner seg typisk når minst ett av følgende gjelder:

    • Store payloads (flere MB) forekommer regelmessig eller kan forekomme.
    • Longrunning-prosesser (Service, Worker) behandler mange payloads, og du observerer minnespikes eller fragmentering.
    • Interop med heterogene systemer: flere partnere, ulike plattformer, av og til feilaktige encodings.
    • Hendelseshistorikk: Det har allerede vært „ødelagte Umlaute“, sporadiske parse-feil eller vanskelig reproducerbare importavbrudd.

    Hvis payloadsene dine er små og kommer fra en kontrollert kilde, er ofte en enkel løsning tilstrekkelig – men selv da: å angi UTF-8 eksplisitt koster nesten ingenting og forhindrer senere overraskelser.

    Avgrensning: Når du trenger ekte streaming

    System.JSON er DOM-orientert. Hvis du vil behandle data virkelig „løpende“, f.eks. et stort Array element for element uten å holde det komplett i minnet, trenger du en annen parser-tilnærming (Tokenizer/SAX). Det er ikke en vurdering av verdi, men et arkitekturvalg:

    • DOM (System.JSON): praktisk, godt for typiske forretningsobjekter, men minnekrevende.
    • Streaming/SAX: lavt minneforbruk, godt for svært store data, men mer implementasjonsarbeid og nøye feilhåndtering.

    Et fornuftig kompromiss er ofte å implementere stream-håndtering, encoding og begrensninger tydelig, og først deretter avgjøre om DOM fortsatt passer. I mange prosjekter stabiliserer dette driften betydelig alene.

    Konklusjon: JSON in Delphi blir pålitelig, når du behandler Encoding og Streams som et eget lag

    De fleste problemene rundt „JSON in Delphi“ handler ikke om JSON-parseren i seg selv, men om den ubemerkede strekningen før: Bytes aus einem Stream werden in Text verwandelt. Hvis du der behandler UTF-8 eksplisitt, gjenkjenner BOM-tilfeller, ikke ignorerer Chunk-Grenzen og setter klare størrelsesbegrensninger, forsvinner de typiske spesialtegnfeilene – og sporadiske parse-problemer blir reproduserbare.

    System.JSON forblir en pragmatisk standard: Ikke den raskeste streaming-parseren, men solid hvis du kontrollerer input og bevisst aksepterer DOM-kostnadene. Hvis du ønsker det, kan vi gå gjennom din konkrete Import-/REST-sti sammen og identifisere stedet hvor Encoding eller Chunking svikter: ta kontakt.

    For dette temaet er også JSON Stream Parser viktige. Innlegget setter disse aspektene i kontekst og viser hva som betyr noe i praksis.

    Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

    Neste trinn

    Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift vurderes samlet allerede tidlig i prosessen.

    Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.

    • Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
    • REST, datatilgang, portaler og utrulling blir ikke utsatt som etterfølgende oppgaver.
    • Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.

    Del innlegg

    Del dette innlegget direkte

    LinkedIn, X, XING, Facebook, WhatsApp og e-post er umiddelbart tilgjengelige. For Instagram forbereder vi lenke og kort tekst umiddelbart.

    E-post

    Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.