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
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:
- Læse bytes fra streamen (kontrolleret, eventuelt med limit/timeout i HTTP-klienten).
- Dekodning til Unicode med eksplicit UTF-8 (BOM kan tolereres).
- 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
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
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.
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.
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.