Från magasinets tema till projektpraxis
Passande tjänste- och tekniksidor för inlägget
„JSON in Delphi“ låter som ett löst problem: System.JSON finns på plats, REST-anrop returnerar text, klart. I praktiken uppstår de verkliga felen där JSON inte föreligger som en bekväm sträng utan som en stream: HTTP-response-stream, filström, Named Pipe, Message-Queue eller ett stort BLOB från databasen. Då sammanfaller tre faktorer som ofta underskattas i vardagen: minnesbeteende, teckenkodning (särskilt UTF-8) och kantfall kring specialtecken.
Detta inlägg visar ett rent, snabbt tillvägagångssätt för att parsa JSON från en TStream utan att skapa onödiga kopior – och framför allt utan de typiska UTF-8-fällorna där tyska umlauter blir „trasiga“ eller parserar sporadiskt avslutas med kryptiska felmeddelanden. Fokus ligger på effekter för drift och gränssnitts-stabilitet: reproducerbar debugging, tydliga begränsningar i tillvägagångssättet och kriterier för när insatsen verkligen är motiverad.
Varför streams beter sig annorlunda vid JSON-parsning i Delphi
Så länge ett JSON-dokument är litet är vägen „läs stream till sträng, parse“ bekväm. När payload-storleken ökar (typiskt: stora listor, rapporter, sync-data, loggexporter) blir det dock kostsamt:
- Duplicerade minneskopior: Du läser bytes till en buffert, konverterar till en Unicode-sträng (Delphi-string = UTF-16), och parsern skapar internt ytterligare strukturer. Det kan kortsiktigt innebära flera kopior.
- GC-/heap-tryck: Många temporära strängar och JSON-värden ökar fragmentering och allocations-overhead, särskilt i långkörande processer (tjänster, workers, importjobb).
- Felbild blir otydlig: Om felaktiga antaganden om kodning görs redan vid inläsning ser JSON-parsaren bara „konstiga tecken“ eller oväntade kontrollbytes.
Viktig är en tydlig åtskillnad: JSON är formellt Unicode, på transporten är det nästan alltid UTF-8. Delphi arbetar internt däremot med UTF-16. Övergången från bytes (Stream) till tecken (String) är den punkt där problem med specialtecken uppstår – inte i JSON självt.
System.JSON: Vad det gör bra – och vad du bör vara uppmärksam på
System.JSON är i Delphi standarden för DOM-baserad JSON: du får ett objektmodell (TJSONObject, TJSONArray), kan läsa värden, iterera och serialisera. Det är robust för typiska affärsintegrationer, men har två konsekvenser:
- Det är ingen riktig streamingparser: Objektmodellen byggs upp helt. Du sparar visserligen eventuellt inläsningen i en extra sträng, men DOM:en förblir minneskrävande.
- Parserns input är i regel text: Beroende på Delphi-version och vald API hamnar man snabbt tillbaka vid strängen, inklusive konvertering av kodning.
Om ditt mål är en „snabb streamparser“ menar du i praktiken oftast en av två saker: (1) inga onödiga kopior och (2) så tidig fail-fast som möjligt vid korrupta payloads. Båda går att uppnå med System.JSON, så länge du kontrollerar steget från byte till text.
UTF-8-fällor vid specialtecken: de typiska orsakerna
Om umlauter (ä/ö/ü/ß) eller andra specialtecken i resultatet ser felaktiga ut (ä, – etc.), är det nästan alltid en kodningsmismatch. I Delphi-miljö är följande orsaker särskilt vanliga:
1) ANSI-fallback genom „bekväma“ hjälpfunktioner
Vissa läsvägar antar tyst systemets ANSI-encoding (kodsida för Windows-systemet) om ingen explicit kodning skickas. Det märks först när ett payload innehåller mer än ASCII. I testdata fungerar det ofta av en slump, i produktion går det sönder vid verkliga namn, platser och fria texter.
2) BOM-förvirring (Byte Order Mark)
UTF-8 kan börja med en BOM (bytes EF BB BF). I webbkontext är BOM ovanligt, men i filer förekommer det. Vissa läsare känner igen BOM och anpassar kodningen, andra gör det inte eller bara i vissa lägen. Om en BOM slinker in i strängen som ett vanligt tecken ser du ofta ett osynligt „Zero Width No-Break Space“ i början eller så misslyckas JSON-parsaren direkt på det första tokenet.
3) Dubbelkonvertering (UTF-8 tolkas „på nytt“)
Det klassiska felmönstret „ä“ istället för „ä“ uppstår när UTF-8-byten först korrekt avkodats till Unicode, men senare tolkas igen som ANSI/UTF-8-byten (eller tvärtom). I Delphi händer det gärna när konverteringarna mellan TBytes, RawByteString och string är otydliga.
4) Avklippning mitt i ett multibyte-tecken
UTF-8 kodar specialtecken i 2–4 byte. Om du läser i chunks (t.ex. 8 KB) och chunk-gränsen ligger mitt i ett tecken måste dekodern buffra korrekt. En naiv metod som konverterar varje chunk separat till en sträng och sedan klistrar ihop dem producerar ogiltiga sekvenser. Det kan framstå som ett „sporadiskt“ fel, beroende på paketgränser, proxybeteende eller HTTP-chunkning.
5) Felaktiga antaganden utifrån HTTP-headrar
I REST är källan ofta Content-Type: application/json; charset=utf-8. Vissa servrar skickar dock inget charset, andra skickar felaktig information. Om du blint litar på headern kan det variera med backend-version. För drift och support är det hjälpsamt att inspektera den faktiska bytestreamen och logga den vid fel.
Ett rent tillvägagångssätt: Stream → UTF-8-decoder → JSON-parser
Den robusta pipelinen består av tre tydliga steg:
- Läs bytes från streamen (kontrollerat, vid behov med limit/tidsgräns i HTTP-klienten).
- Dekodning till Unicode med explicit UTF-8 (BOM kan tolereras).
- Parsning med System.JSON till en objektmodell eller för riktad extraktion.
Den viktigaste hävstången är steg 2: du vill inte att någonstans „Default Encoding“ bestämmer. I Delphi betyder det: ange TEncoding.UTF8 uttryckligen och lita inte på implicita konverteringar.
Vad „snabbt“ konkret innebär här
Med System.JSON kommer du inte att „optimera bort“ DOM:en. Men du kan undvika:
- en ytterligare kopia av hela Payload som en mellansträng, när du internt ändå bara behöver några få värden (då är snarare en annan parser motiverad; mer om det senare),
- upprepad omkodning,
- och du kan läsa in mycket stora payloads kontrollerat (med storleksgräns och ett tydligt felmeddelande), istället för att sluta i Out-of-Memory eller Access Violations.
Det konkreta randfallet: specialtecken korrupta, men bara ibland
Ett praktiskt randfall är särskilt förrädiskt: payloaden är i grunden giltig UTF-8-JSON, men du läser den i chunkar och konverterar varje chunk till en sträng. Så länge endast ASCII förekommer märker du inget. Så snart en Umlaut ligger exakt vid en chunk-gräns uppstår ogiltiga UTF-8-sekvenser. Resultat: antingen korrupta tecken eller ett parse-fel på en plats som inte stämmer med det egentliga innehållet.
Hur du känner igen det:
- Parsefel uppstår „slumpmässigt“ vid stora svar, inte vid små.
- Samma Request fungerar ibland, ibland inte (beroende på Chunking/Transport).
- En hexdump av byten visar giltig UTF-8, men din loggade sträng innehåller Replacement Characters (�) eller klassisk Mojibake.
Lösningen är inte att använda „mer readln“ eller „större buffertar“, utan att använda en decoder som korrekt buffrar multibyte-sekvenser över chunk-gränser. Det är just där TStreamReader i kombination med en UTF-8-Encoding kan vara till hjälp – om du initialiserar den korrekt.
Praktisk guide: kontrollera UTF-8 reproducerbart i Delphi
Innan du skruvar på parsaren behöver du en felsökningsuppsättning som visar den faktiska byteföljden. För Support och drift är det ovärderligt, eftersom du senare tydligt kan säga om motparten levererar felaktiga data eller om din pipeline dekodar fel.
1) Kontrollera de första bytena (BOM, JSON-start)
Om JSON:en kommer med BOM ser du i början av strömmen EF BB BF. Direkt därefter bör typiskt ‚{‚ eller ‚[‚ följa. Om ‚‘ redan finns i strängen har BOM inte behandlats som BOM utan dekodats som text.
2) Logga råbytes i hex – men begränsat
Logga inte kompletta payloads i produktion (Datenschutz, kostnader, loggvolym). Följande har visat sig fungera:
- Prefix (t.ex. de första 256 eller 1024 bytena),
- Suffix (sista 256 bytena),
- och en hash (SHA-256) för korrelation när du behöver jämföra payloads.
Med det kan du ofta diagnosticera problem med specialtecken på minuter: Är byteföljden för „ä“ korrekt (C3 A4)? Föreligger en trunkering? Dyker ett oväntat 0x00 (Nullbyte) upp, t.ex. på grund av ett felaktigt antagande om UTF-16?
3) Logga Content-Type och charset
Vid HTTP/REST: Logga Content-Type och det deklarerade charset. Om byte-sekvensen tydligt är UTF-8 men charset påstår något annat, bör du inte följa det blint i klienten. För JSON är UTF-8 de facto-standarden. Vid tvekan: byte-analys vinner över header.
Snabb Stream-Parser med System.JSON: Design utan onödiga kopior
Ett praktiskt mönster är: Du läser från streamen till en byte-buffert, bygger därav en String exakt en gång med UTF-8, och ger denna String till JSON-parsaren. Det är inte „Streaming“ i SAX-meningen, men det är en kontrollerad, prestandaeffektiv pipeline utan överraskande kodningsväxlingar.
Viktigt för arkitekturen: Utforma funktionen så att den centralt avgör följande:
- Vilken kodningsregel som gäller (vanligtvis UTF-8, BOM valfritt)?
- Hur stor payloaden får vara högst (DoS-skydd, driftgräns)?
- Hur felmeddelanden ser ut (med kontext, men utan dataläckage)?
Inläsningsstrategi: Begränsad buffring istället för „StreamToString“ utan gräns
Om du accepterar JSON från externa källor (partners, mobila klienter, tredjepartsleverantörer) är en storleksgräns obligatorisk. Utan gräns räcker en enda olycklig förfrågan för att utsätta en tjänst för minnespåfrestning. I praktiken innebär det: räkna summan av bytes vid läsning och avbryt vid en gräns – med ett tydligt undantag som är begripligt i övervakningen.
Varför jag ofta förväntar mig UTF-8 utan BOM, men skulle tolerera en BOM
I REST-payloads förekommer BOM sällan. I filer (exporter, manuell redigering) däremot oftare. För robusta importvägar är det rimligt att tolerera BOM, men att göra det synligt i loggen, eftersom det kan indikera „fil-världen“ snarare än „API-världen“.
De tysta felen: TStreamReader-standardvärden och Text-Reader i blandad drift
TStreamReader är bekväm, men du måste ha två saker under kontroll:
- Sätt encoding explicit: Förlita dig inte på att den „känner igen“ det.
- Förstå buffring: Readern buffrar internt. Om du senare läser samma stream på annat ställe är positionen relevant. Det låter trivialt, men blir snabbt en felkälla i större importpipelines.
Särskilt besvärligt är blandad drift: först läsa en del som bytes (t.ex. för loggning eller magic-bytes), sedan fortsätta med TStreamReader. Om du då inte noggrant backar eller initialiserar readern vid rätt position, läser du från byte 257 istället för från 0. JSON-parsaren rapporterar då „Invalid character at position …“, även om payloaden i sig är korrekt.
När specialtecken förblir „trasiga“ trots UTF-8: Escaping vs. verkligt Unicode
JSON kan innehålla specialtecken på två sätt:
- Som äkta UTF-8-tecken (t.ex. „München“ som bytes C3 BC …).
- Som escape-sekvens (z. B. „Mu00fcnchen“).
Båda är giltiga. I praktiken är det viktigt: escape-sekvenser kringgår många transportproblem, men de döljer bara till synes kodningsfel. Om ditt system någonstans tolkar bytes fel är det en driftsrisk, inte bara en kosmetisk bugg. Dessutom kan escape-sekvenser vara förvirrande i loggning/monitoring om team förväntar sig att se „läsbar text“.
System.JSON levererar i båda fallen i slutändan normala Delphi-strängar (UTF-16), förutsatt att vägen dit var korrekt.
Bedöm prestanda realistiskt: DOM-kostnader, stora arrayer och selektivitet
Den största prestandavinsten är ofta inte att „göra parsern snabbare“, utan parsa mindre. Med System.JSON är det svårt eftersom du får DOM. Tre typiska situationer:
- Stora arrayer (10.000+ element): Att bygga upp DOM tar tid och RAM. Om du bara behöver 2 fält per element är en streamningskapabel parser (SAX/Tokenizer) ofta mer lämplig. System.JSON är inte gjort för det.
- Enskilda objekt med många fält: Om du bara behöver ett fåtal fält kan du ändå använda DOM, men undvik att traversera flera gånger. Läs värden en gång och mappa dem till dina strukturer.
- Flera stora payloads i följd: I importjobb eller sync-worker är det värt att kapsla in parsningen i ett tydligt steg och efter varje dokument frigöra alla referenser så att minneshanteraren kan rensa upp. Det är trivialt, men i tjänster görs det gärna „vid sidan av“.
En „snabb stream-parser“ med System.JSON är därför ofta en bra kompromiss: läs in effektivt och korrekt, använd DOM medvetet, definiera gränser. Om du behöver riktig streaming-semantik (t.ex. bearbeta array-element ett efter ett utan att hålla allt i minnet) är System.JSON inte rätt grund.
Robusthet i drift: felmönster och hur du gör dem omedelbart hanterbara
JSON-parsningsfel i loggar är ofta inte användbara eftersom de bara anger en position. För drift och support behöver du kontext:
- Byte-position vs. tecken-position: Vid UTF-8 är detta inte identiskt. Om parsaren rapporterar en teckenposition kan bytepositionen avvika. För byte-dumps är bytepositionen avgörande.
- Snippet kring felstället: Logga vid fel ett litet fönster runt positionen (t.ex. 40 tecken före/efter), men endast om inga känsliga data ingår. Alternativt: logga endast hexadecimalt.
- Korrelation: Request-ID, Endpoint, Partner-ID, Payload-Hash. Annars hittar du „det där enda felet“ aldrig igen.
Målet är att du efter en produktionsincident inom några minuter ska kunna svara: „Encoding felaktigt tolkat“, „Payload avklippt“, „Server levererar invalid JSON“ eller „vi har ett Mapping-Problem“.
Undvik UTF-8-fallgropar: Checklista
- Teckenkodning alltid explicit: Vid läsning från stream och vid skrivning till loggar/filer, lita inte på standardinställningar.
- Ingen chunk-till-string-konvertering: Om du läser i chunked form, samla bytes eller använd en decoder som buffrar multibyte-sekvenser.
- BOM-tolerant men synlig: Acceptera, men kunna identifiera vid felsökning.
- Sätt gränser: Max. payload-storlek, max. objekt-/array-djup (om du kan kontrollera det), timeouts i HTTP-klienten.
När lönar sig ansträngningen för en stream-parser egentligen?
Du behöver inte optimera varje JSON-ställe. Angreppssättet lönar sig typiskt när minst en av följande punkter gäller:
- Stora payloads (flera MB) förekommer regelbundet eller kan förekomma.
- Longrunning-Prozesse (Service, Worker) bearbetar många payloads och du observerar minnespikar eller fragmentering.
- Interop med heterogenen System: flera partner, olika plattformar, sporadiskt felaktiga teckenkodningar.
- Incident-Historie: det har redan förekommit fel med specialtecken, sporadiska parsefel eller svårt reproducerbara importavbrott.
Om dina payloads är små och kommer från en kontrollerad källa räcker ofta en enkel lösning – men även då: att explicit ange UTF-8 kostar nästan ingenting och förhindrar senare överraskningar.
Avgränsning: När du verkligen behöver streaming
System.JSON är DOM-orienterat. Om du verkligen vill bearbeta data löpande, t.ex. ett stort array element för element utan att hålla det helt i minnet, behöver du en annan parserstrategi (Tokenizer/SAX). Det är inget värdeomdöme utan ett arkitekturval:
- DOM (System.JSON): bekvämt, bra för typiska affärsobjekt, men minnesintensivt.
- Streaming/SAX: låg minnesförbrukning, lämpligt för mycket stora datamängder, men kräver mer implementationsarbete och noggrannare felhantering.
En rimlig kompromiss är ofta: bygg ordentlig stream-hantering, hantera encoding och sätt tydliga gränser, och avgör först därefter om DOM fortfarande passar. I många projekt stabiliserar detta driften avsevärt.
Slutsats: JSON in Delphi blir tillförlitligt om du behandlar Encoding och Streams som ett eget lager
De flesta problem kring „JSON in Delphi“ beror inte på själva JSON-parsaren, utan på den obemärkta sträckan innan: bytes från en stream omvandlas till text. Om du där explicit hanterar UTF-8, känner igen BOM-fall, inte ignorerar chunk-gränser och sätter tydliga storleksgränser, försvinner de typiska felen med specialtecken – och sporadiska parseproblem blir reproducerbara.
System.JSON förblir en pragmatisk standard: inte den snabbaste streaming-parsaren, men solid om du kontrollerar input och medvetet accepterar DOM-kostnaderna. Om du vill kan vi tillsammans gå igenom din konkreta import-/REST-väg och identifiera den punkt där Encoding eller Chunking fallerar: Ta kontakt.
För detta ämne är även JSON Stream Parser viktiga. Inlägget placerar dessa aspekter på ett begripligt sätt och visar vad som är viktigt i praktiken.
nästa steg
När ett ämne blir ett verkligt projekt bör arkitektur, befintligt bestånd och drift tidigt ses över gemensamt.
Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.
- Nuläge, målbild och tekniska risker bedöms tillsammans.
- REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
- Ni ser tidigt vilken väg som är ekonomiskt och driftmässigt hållbar.