Net-Base Magasin

13.08.2026

JSON i Delphi: Hurtig stream-parser med System.JSON + UTF-8-faldgruber ved specialtegn

Når JSON-payloads i Delphi kommer direkte fra en stream, bliver „lige at parse“ hurtigt et produktionsproblem: stort hukommelsesforbrug, sporadiske parse-fejl og ødelagte Umlaute. Denne praksisartikel viser, hvordan du med System.JSON opbygger en hurtig stream-parser...

13.08.2026

Fra magasinets tema til projektpraksis

Passende service- og tekniske sider til artiklen

„JSON in Delphi“ lyder som et løst problem: System.JSON er om bord, REST-kald leverer tekst, færdigt. I praksis opstår de reelle fejl dog dér, hvor JSON ikke foreligger som en bekvem streng, men som en stream: HTTP-response-stream, filstream, named pipe, message-queue eller en stor BLOB fra databasen. Så mødes tre forhold, som ofte undervurderes i dagligdagen: hukommelsesadfærd, tegnkodning (især UTF-8) og kanttilfælde omkring specialtegn.

Dette indlæg viser en ren, hurtig tilgang til at parse JSON fra en TStream uden at skabe unødvendige kopier – og frem for alt uden de typiske UTF-8-faldgruber, hvor Umlaut-tegn kan blive „ødelagt“ eller parseren sporadisk afslutter med kryptiske fejlmeldinger. Fokus ligger på konsekvenser for drift og grænsefladestabilitet: reproducerbar debugging, klare grænser for tilgangen og kriterier for, hvornår indsatsen virkelig er berettiget.

Hvorfor streams opfører sig anderledes ved JSON-parsing i Delphi

Så længe et JSON-dokument er lille, er vejen „læs stream til streng, så parse“ bekvem. Når payload-størrelsen når et vist niveau (typisk: store lister, rapporter, sync-data, log-eksporter), bliver det dog dyrt:

  • Hukommelsesduplikater: Du læser bytes ind i en buffer, konverterer til en Unicode-streng (Delphi-streng = UTF-16), parseren opbygger internt yderligere strukturer. Det kan kortvarigt betyde flere kopier.
  • GC/heap-tryk: Mange midlertidige strenge og JSON-værdier øger fragmentering og allocations-overhead, særligt i langkørende processer (services, workers, import-jobs).
  • Fejlbilledet bliver uklart: Hvis der ved indlæsning allerede er truffet forkerte encoding-antagelser, ser JSON-parseren kun „mærkelige tegn“ eller uventede styrebytes.

Vigtigt er en klar adskillelse: JSON er formelt Unicode, på transportlaget er det næsten altid UTF-8. Delphi arbejder internt dog med UTF-16. Overgangen fra bytes (Stream) til tegn (String) er det sted, hvor problemer med specialtegn opstår – ikke i JSON selv.

System.JSON: Hvad det gør godt – og hvor du skal være opmærksom

System.JSON er i Delphi standarden for DOM-baseret JSON: Du får et objektmodel (TJSONObject, TJSONArray), kan forespørge værdier, iterere og serialisere. Det er robust for typiske forretningsintegrationer, men har to konsekvenser:

  • Det er ikke en ægte streaming-parser: Objektmodellen bygges fuldstændigt op. Du sparer muligvis indlæsningen til en ekstra streng, men DOM’et forbliver hukommelsesintensivt.
  • Parser-input er som regel tekst: Afhængigt af Delphi-version og den anvendte API ender man hurtigt igen med en streng, inklusive encoding-konvertering.

Hvis dit mål er en „hurtig Stream-Parser“, mener du i praksis som regel ét af to ting: (1) ingen unødvendige kopier og (2) så tidlig fail-fast som muligt ved beskadigede payloads. Begge dele er opnåelige med System.JSON, så længe du kontrollerer byte-til-tekst-trinnet.

UTF-8-faldgruber ved specialtegn: de typiske årsager

Abstrakte Grafik zeigt UTF-8-Mehrbytezeichen über Chunk-Grenzen und korrekte Pufferung im Decoder vor dem JSON-Parsing
Chunking er uskadeligt – så længe UTF-8-dekoderen buffrer flerbyte-sekvenser over grænserne.

Hvis Umlaute (ä/ö/ü/ß) eller andre specialtegn i resultatet ser forkert ud (ä, – osv.), er det næsten altid et kodningsmismatch. I Delphi-miljøet forekommer disse årsager særligt hyppigt:

1) ANSI-fallback via „bekvemme“ hjælpefunktioner

Nogle læseveje antager stiltiende systemets ANSI-encoding (codepage på Windows-systemet), hvis der ikke angives en eksplicit kodning. Det bliver først synligt, når et payload indeholder andet end ASCII. I testdata er det ofte tilfældigt „okay“, i produktion går det galt ved rigtige navne, steder og fritekster.

2) BOM-forvirring (Byte Order Mark)

UTF-8 kan starte med en BOM (bytes EF BB BF). I webkontekst er BOM sjældent; ved filer forekommer det. Nogle readers genkender BOM og tilpasser kodningen, andre gør det ikke eller kun i bestemte tilstande. Hvis en BOM glider ind i strengen som et almindeligt tegn, ser du ofte et usynligt „Zero Width No-Break Space“ i begyndelsen, eller JSON-parseren fejler allerede ved det første token.

3) Dobbeltkonvertering (UTF-8 bliver „igen“ fortolket)

Det klassiske fejlbillede „ä“ i stedet for „ä“ opstår, når UTF-8-bytes først er korrekt dekodet til Unicode, men senere igen fejltolkes som ANSI/UTF-8-bytes (eller omvendt). I Delphi sker det ofte, når der er uklar konvertering mellem TBytes, RawByteString og string.

4) Trunkering midt i et multibyte-tegn

UTF-8 koder specialtegn i 2–4 bytes. Hvis du læser i chunks (fx 8 KB) og chunk-grænsen ligger midt i tegnet, skal dekoderen buffre det korrekt. En naiv tilgang, der konverterer hver chunk separat til en streng og limer dem sammen, producerer ugyldige sekvenser. Det kan fremstå som en „sporadisk“ fejl afhængigt af pakkegrænser, proxy-adfærd eller HTTP-chunking.

5) Forkerte antagelser fra HTTP-headere

I REST er kilden ofte Content-Type: application/json; charset=utf-8. Nogle servere leverer dog ingen charset, andre leverer forkerte angivelser. Hvis du blindt stoler på headeren, kan det skifte afhængigt af backend-version. For drift og support er det nyttigt at kontrollere den faktiske byte-strøm og logge ved fejl.

En ren tilgang: Stream → UTF-8-dekoder → JSON-parser

Den robuste pipeline består af tre klare trin:

  1. Læse bytes fra streamen (kontrolleret, eventuelt med limit/timeout i HTTP-klienten).
  2. Dekodning til Unicode med eksplicit UTF-8 (BOM kan tolereres).
  3. Parsing med System.JSON til et objektmodel eller til målrettet udtræk.

Det vigtigste greb er trin 2: Du vil ikke have, at et „Default Encoding“ beslutter et sted. I Delphi betyder det: angiv TEncoding.UTF8 eksplicit og stol ikke på implicitte konverteringer.

Hvad „schnell“ her konkret betyder

Med System.JSON fjerner du ikke DOM’en. Men du kan undgå:

  • en ekstra kopi af hele Payloaden som en mellemliggende streng, hvis du internt alligevel kun behøver få værdier (der kan en anden parser være mere relevant; mere om det senere),
  • gentagen omkodning,
  • og du kan indlæse meget store Payloads kontrolleret (med størrelsesgrænse og klar fejlbesked), i stedet for at ende i Out-of-Memory eller Access Violations.

Et konkret randtilfælde: Specialtegn ødelagt, men kun nogle gange

Et praksis-eksempel er særligt snigende: Payloaden er i udgangspunktet gyldigt UTF-8-JSON, men du læser den i chunks og konverterer per chunk til en streng. Så længe kun ASCII forekommer, bemærker du intet. Så snart en Umlaut ligger præcis på en chunk-grænse, opstår ugyldige UTF-8-sekvenser. Resultatet: enten ødelagte tegn eller en Parse-fejl på et sted, der ikke svarer til det egentlige indhold.

Hvordan du kan genkende det:

  • Parse-fejl opstår „tilfældigt“ ved store svar, ikke ved små.
  • Den samme Request virker nogle gange, nogle gange ikke (afhængig af Chunking/Transport).
  • En Hexdump af bytes viser gyldig UTF-8, men din loggede streng indeholder Replacement Characters (�) eller klassisk Mojibake.

Løsningen er ikke at „readln“ mere eller bruge „større Buffer“, men at bruge en decoder, der korrekt buffrer multibyte-sekvenser over chunk-grænser. Netop her kan TStreamReader i kombination med en UTF-8-Encoding være nyttig – hvis du initialiserer den korrekt.

Praktisk vejledning: Reproducerbar kontrol af UTF-8 i Delphi

Debugging-setup med byte-dump-udskrifter og arbejdsmateriale til at kontrollere UTF-8-bytes og BOM i JSON-Payloads
For ren debugging er bytestrømmen det første, der tæller: BOM, trunkering og ugyldige sekvenser bliver hurtigt synlige.

Før du begynder at skrue på parseren, har du brug for et debugging-setup, der viser den faktiske bytefølge. For support og drift er det guld værd, fordi du senere klart kan sige, om modparten leverer forkerte data eller om din pipeline dekoder forkert.

1) Tjek de første bytes (BOM, JSON-start)

Hvis JSON kommer med BOM, ser du EF BB BF i starten af streamen. Umiddelbart derefter bør typisk „{“ eller „[“ komme. Hvis der allerede står „“ i strengen, er BOM ikke blevet behandlet som BOM, men decodet som tekst.

2) Log råbytes i hex – men begrænset

Log ikke komplette Payloads i produktion (databeskyttelse, omkostninger, log-volumen). Følgende har vist sig nyttigt:

  • Prefix (fx første 256 eller 1024 bytes),
  • Suffix (sidste 256 bytes),
  • og en hash (SHA-256) til korrelation, hvis du skal sammenligne Payloads.

Dermed kan du ofte klassificere problemer med specialtegn på få minutter: Er bytefølgen for „ä“ korrekt (C3 A4)? Forekommer en trunkering? Dukker en uventet 0x00 (nullbyte) op, f.eks. på grund af fejlagtig antagelse om UTF-16?

3) Log Content-Type og Charset

Ved HTTP/REST: Log Content-Type og det deklarerede charset. Hvis bytes tydeligt er UTF-8, men charset påstår noget andet, bør du som klient ikke følge det blindt. For JSON er UTF-8 de facto-standarden. I tvivlstilfælde: byte-analyse slår headeren.

Hurtig Stream-Parser med System.JSON: Design uden unødvendige kopier

Skematisk fremstilling af en pipeline bestående af Stream, UTF-8-dekodning, størrelsesbegrænsning og JSON-DOM-parsing i Delphi
En klar pipeline med limit og eksplicit UTF-8 adskiller transport, dekodning og parsing tydeligt.

Et praktikabelt mønster er: Du læser fra streamen ind i en byte-buffer, bygger deraf en String præcis én gang med UTF-8, og giver denne String til JSON-parseren. Det er ikke „streaming“ i SAX-forstand, men det er en kontrolleret, performant pipeline uden overraskende encoding-skift.

Vigtigt for arkitekturen: Byg funktionen, så den centralt beslutter ét sted:

  • Hvilken encoding-regel gælder (som regel UTF-8, BOM valgfrit)?
  • Hvor stor må payload maksimalt være (DoS-beskyttelse, driftsgrænse)?
  • Hvordan ser fejlmeddelelser ud (med kontekst, men uden datalæk)?

Indlæsningsstrategi: Begrænset buffring i stedet for „StreamToString“ uden limit

Hvis du accepterer JSON fra eksterne kilder (partnere, mobile clients, tredjepart), er et størrelseslimit påkrævet. Uden limit kan en enkelt uheldig request skabe Memory Pressure på en service. Praktisk betyder det: ved læsning skal du tælle summen af bytes og afbryde ved en grænse – med en tydelig Exception, der er meningsfuld i overvågning.

Hvorfor jeg ofte forventer UTF-8 „uden BOM“, men vil tolerere BOM

I REST-payloads forekommer BOM sjældent. I filer (eksporter, manuel redigering) derimod oftere. For robuste importstier er det fornuftigt at tolerere BOM, men logge det synligt, fordi det kan være et tegn på „fil-verden“ frem for „API-verden“.

De stille dræbere: TStreamReader-standardindstillinger og tekst-readere i blandet brug

TStreamReader er bekvem, men du skal have to ting på plads:

  • Angiv encoding eksplicit: Forvent ikke, at den „genkender“ det.
  • Forstå buffering: Readeren bufferer internt. Hvis du læser den samme stream et andet sted senere, er positionen relevant. Det lyder trivielt, men bliver hurtigt en fejlkilde i større importpipelines.

Især problematisk er blandet brug: Først læs en del som bytes (fx til logging eller magic-bytes), og fortsæt derefter med TStreamReader. Hvis du ikke korrekt spoler tilbage eller initialiserer readeren ved den rigtige position, læser du fra byte 257 i stedet for fra 0. JSON-parseren rapporterer så „Invalid character at position …“, selvom payloaden i sig selv er korrekt.

Når specialtegn forbliver „ødelagte“ trods UTF-8: Escaping vs. ægte Unicode

JSON kan indeholde specialtegn på to måder:

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

Begge dele er gyldige. I praksis er det vigtigt: Escape-sekvenser omgår mange transportproblemer, men de skjuler kun tilsyneladende encoding-fejl. Hvis dit system et eller andet sted fortolker bytes forkert, er det en driftsrisiko, ikke bare en kosmetisk fejl. Derudover kan escape-sekvenser forvirre ved logning/monitorering, hvis teams forventer at se „læselig tekst“.

System.JSON leverer i begge tilfælde til sidst normale Delphi-strenge (UTF-16), forudsat at vejen dertil var korrekt.

Realistisk performancevurdering: DOM-omkostninger, store arrays og selektivitet

Den største performancegevinst er ofte ikke at gøre ‚parseren hurtigere‘, men at parse mindre. Med System.JSON er det vanskeligt, fordi du får DOM’en. Tre typiske situationer:

  • Store arrays (10.000+ elementer): Opbygning af DOM koster tid og RAM. Hvis du kun behøver 2 felter pr. element, er en streaming-venlig parser (SAX/Tokenizer) ofte mere fornuftig. System.JSON er ikke lavet til det.
  • Enkeltobjekter med mange felter: Hvis du kun har brug for få felter, kan du stadig bruge DOM’en, men undgå gentagen traversering. Hent værdierne én gang og map dem ind i dine strukturer.
  • Flere store payloads efter hinanden: I import-jobs eller sync-workers betaler det sig at kapsle parsing i et klart trin og frigive alle referencer efter hvert dokument, så memory-manageren kan rydde op. Det er banalt, men i services gøres det ofte ‚ved siden af‘.

En ‚hurtig stream-parser‘ sammen med System.JSON er derfor ofte et godt kompromis: Læs effektivt og korrekt ind, brug DOM bevidst, definer grænser. Hvis du har brug for ægte streaming-semantik (f.eks. behandle array-elementer ét ad gangen uden at holde alt i hukommelsen), er System.JSON ikke det rigtige fundament.

Robusthed i drift: Fejlscenarier og hvordan du gør dem umiddelbart sporbare

JSON-parse-fejl i logs er ofte ubrugelige, fordi de kun angiver en position. Til drift og support har du brug for kontekst:

  • Byte-position vs. tegn-position: Ved UTF-8 er de ikke det samme. Hvis parseren rapporterer en tegn-position, kan byte-positionen afvige. Til byte-dumps er byte-positionen afgørende.
  • Snippet omkring fejlen: Log et lille vindue omkring positionen ved fejl (fx 40 tegn før/efter), men kun hvis der ikke er sensitive data. Alternativt: log kun hexadecimalt.
  • Korrelation: Request-ID, Endpoint, Partner-ID, Payload-Hash. Ellers finder du aldrig „den ene fejl“ igen.

Formålet er, at du efter en produktionsincident inden for få minutter kan besvare: „Encoding fortolket forkert“, „Payload afskåret“, „Server leverer invalid JSON“ eller „vi har et mapping-problem“.

Undgå UTF-8-faldgruber målrettet: Tjekliste

  • Angiv altid encoding eksplicit: Ved læsning fra stream og ved skrivning til logs/filer, stol ikke på defaults.
  • Ingen chunk-til-string-konvertering: Hvis du læser i chunks, så opsaml bytes eller brug en decoder, der bufferer multibyte-sekvenser.
  • BOM-tolerant, men synlig: Accepter, men vær i stand til at opdage det under debugging.
  • Sæt grænser: Maks. payload-størrelse, maks. objekt-/array-dybde (hvis du kan kontrollere det), timeouts i HTTP-clienten.
  • Adskilte ansvarsområder: kapsl „transportlæsning“ og „JSON-parsing“ hver for sig. Så er fejlsøgning hurtigere, og man kan senere udskifte en parser.
  • Hvornår er indsatsen for en stream-parser virkelig berettiget?

    Du behøver ikke optimere alle JSON-forekomster. Tilgangen lønner sig typisk, hvis mindst ét af følgende gør sig gældende:

    • Store Payloads (flere MB) forekommer regelmæssigt eller kan forekomme.
    • Langløbende processer (Service, Worker) behandler mange Payloads, og du observerer Memory-Spikes eller fragmentering.
    • Interop med heterogene systemer: flere partnere, forskellige platforme, lejlighedsvis fejlbehæftede encodninger.
    • Incident-historik: Der har allerede været „ødelagte Umlaute“, sporadiske parse-fejl eller svært reproducerbare importafbrydelser.

    Hvis dine Payloads er små og kommer fra en kontrolleret kilde, er en simpel løsning ofte tilstrækkelig – men også da: at sætte UTF-8 eksplicit koster næsten ingenting og forhindrer senere overraskelser.

    Afgrænsning: Når du har brug for ægte streaming

    System.JSON er DOM-orienteret. Hvis du vil behandle data „løbende“, f.eks. et stort Array element for element uden at holde det hele i hukommelsen, har du brug for en anden parser-tilgang (Tokenizer/SAX). Det er ikke en vurdering, men en arkitekturbeslutning:

    • DOM (System.JSON): bekvemt, godt til typiske business-objekter, men hukommelseskrævende.
    • Streaming/SAX: lavt hukommelsesforbrug, godt til meget store datamængder, men større implementeringsindsats og mere omhyggelig fejlbehandling.

    Et fornuftigt kompromis er ofte: at implementere stream-håndtering, encoding og grænser ordentligt, og først derefter beslutte, om DOM stadig passer. I mange projekter stabiliserer det alene driften mærkbart.

    Konklusion: JSON i Delphi bliver pålideligt, hvis du behandler encoding og streams som et selvstændigt lag

    De fleste problemer omkring „JSON i Delphi“ skyldes ikke JSON-parseren i sig selv, men det ubemærkede led før den: Bytes fra en stream bliver omdannet til tekst. Hvis du dér håndterer UTF-8 eksplicit, genkender BOM-tilfælde, ikke ignorerer chunk-grænser og sætter klare størrelsesgrænser, forsvinder de typiske specialtegnsfejl – og sporadiske parse-fejl bliver reproducerbare.

    System.JSON forbliver en pragmatisk standard: ikke den hurtigste streaming-parser, men solid, hvis du kontrollerer inputtet og bevidst accepterer DOM-omkostninger. Hvis du ønsker det, kan vi gennemgå din konkrete import-/REST-sti sammen og identificere det sted, hvor encoding eller chunking fejler: tage kontakt.

    JSON-stream-parsere er også vigtige i denne sammenhæng. Indlægget sætter disse aspekter i perspektiv og viser, hvad der betyder noget i praksis.

    Drøft projekt eller moderniseringsforløb med Net-Base.

    Næste trin

    Når emnet bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.

    Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.

    • Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
    • REST, dataadgang, portaler og udrulning bliver ikke udskudt som efterfølgende opgaver.
    • De ser tidligt, hvilken vej der er økonomisk og driftsmæssigt bæredygtig.

    Del indlæg

    Del dette indlæg direkte

    LinkedIn, X, XING, Facebook, WhatsApp og e-mail er straks tilgængelige. Til Instagram forbereder vi link og kort tekst.

    E-mail

    Instagram åbner i en ny fane. Linket og kortteksten kopieres på forhånd til udklipsholderen.