De la tema din revistă la practica în proiecte
Pagini relevante de servicii și pagini tehnice pentru articol
„JSON in Delphi“ sună ca o problemă rezolvată: System.JSON este inclus, apelurile REST returnează text, gata. În practică, erorile reale apar acolo unde JSON nu este disponibil ca un string comod, ci ca un Stream: stream de răspuns HTTP, stream de fișier, Named Pipe, Message-Queue sau un BLOB mare din baza de date. Apoi se întâlnesc trei aspecte care sunt adesea subestimate în activitatea zilnică: comportamentul memoriei, codarea caracterelor (în special UTF-8) și cazurile limită legate de caractere speciale.
Acest articol arată o abordare curată și rapidă pentru a parsa JSON dintr-un TStream fără a genera copii inutile – și mai ales fără capcanele tipice UTF-8, în care diacriticile (Umlaute) sunt „stricate“ sau parserele ies sporadic cu mesaje criptice. Accentul este pe impactul asupra operării și stabilității interfețelor: debugging reproducibil, limite clare ale abordării și criterii când efortul merită cu adevărat.
De ce Streams se comportă diferit la parsarea JSON în Delphi
Atâta vreme cât un document JSON este mic, calea „Stream in String lesen, dann parse“ este comodă. Dar de la o anumită mărime a payload-ului (tipic: liste mari, rapoarte, date de sincronizare, exporturi de loguri) devine costisitor:
- Duplicări de memorie: Citești octeți într-un buffer, convertești într-un string Unicode (Delphi-String = UTF-16), parserul generează intern structuri suplimentare. Aceasta poate însemna temporar mai multe copii.
- Presiune GC/Heap: Multe stringuri temporare și valori JSON cresc fragmentarea și overhead-ul de alocare, în special în procese care rulează mult timp (servicii, workeri, joburi de import).
- Imaginea erorii devine neclară: Dacă la citire se fac presupuneri greșite despre encoding, parserul JSON va vedea doar „caractere ciudate“ sau octeți de control neașteptați.
Este importantă o separare clară: JSON este formal Unicode, pe rețea este aproape întotdeauna UTF-8. Delphi lucrează însă intern cu UTF-16. Tranziția de la octeți (Stream) la caractere (String) este locul unde apar problemele cu caractere speciale – nu în JSON în sine.
System.JSON: Ce face bine – și unde trebuie să fii atent
System.JSON este în Delphi standardul pentru JSON bazat pe DOM: primești un model de obiecte (TJSONObject, TJSONArray), poți interoga valori, itera, serializa. Aceasta este robustă pentru integrări business tipice, dar are două consecințe:
- Nu este un parser streaming real: Modelul de obiecte este construit complet. Poți evita eventual citirea într-un string suplimentar, dar DOM-ul rămâne intensiv din punct de vedere al memoriei.
- Intrarea parserului este, în general, text: În funcție de versiunea Delphi și de API-ul folosit, ajungi rapid din nou la string, inclusiv conversia encoding-ului.
Dacă obiectivul tău este „schneller Stream-Parser“, în practică te referi de obicei la unul din două lucruri: (1) niciun fel de copii inutile și (2) fail-fast cât mai devreme posibil la payload-urile afectate. Ambele sunt realizabile cu System.JSON, atât timp cât controlezi etapa byte-în-text.
Capcanele UTF-8 pentru caractere speciale: cauzele tipice
Dacă diacriticele (ä/ö/ü/ß) sau alte caractere speciale apar incorect în rezultat (ä, – etc.), este aproape întotdeauna un mismatch de encoding. În contextul Delphi cauzele acestea apar mai frecvent:
1) ANSI-Fallback durch „bequeme“ Helper
Unele căi de citire acceptă tacit encoding-ul ANSI al sistemului (codepage al sistemului Windows) dacă nu se transmite o codare explicită. Problema devine vizibilă abia când un payload conține altceva decât ASCII. În datele de test asta e adesea întâmplător „okay“, iar în producție provoacă erori la nume reale, locații sau texte libere.
2) BOM-Verwirrung (Byte Order Mark)
UTF-8 poate începe cu un BOM (octeții EF BB BF). În context web BOM-ul e neobișnuit, dar apare la fișiere. Unii readeri recunosc BOM și adaptează encoding-ul, alții nu sau doar în anumite moduri. Dacă un BOM ajunge tratat ca un caracter obișnuit în string, vezi adesea un invizibil „Zero Width No-Break Space“ la început sau JSON-parserul eșuează imediat la primul token.
3) Doppel-Konvertierung (UTF-8 wird „nochmal“ interpretiert)
Tipicul simptom „ä“ în loc de „ä“ apare când octeții UTF-8 au fost decodați corect în Unicode, dar ulterior sunt interpretați din nou greșit ca octeți ANSI/UTF-8 (sau invers). În Delphi se întâmplă frecvent când conversiile între TBytes, RawByteString și string nu sunt clar delimitate.
4) Trunkierung mitten in einem Multibyte-Zeichen
UTF-8 codifică caracterele speciale în 2–4 octeți. Dacă citești în chunks (ex. 8 KB) și limita chunk-ului cade în mijlocul unui caracter, decoderul trebuie să facă buffering corect. Un algoritm naiv care convertește fiecare chunk separat în string și apoi le lipește produce secvențe invalide. Aceasta poate părea o eroare „sporadică“, dependentă de granițe de pachete, comportamentul proxy-ului sau HTTP-Chunking.
5) Falsche Annahmen aus HTTP-Headern
La REST sursa este adesea Content-Type: application/json; charset=utf-8. Totuși, unii servere nu trimit charset, alții trimit informații incorecte. Dacă folosești orbeste header-ul, comportamentul poate varia între versiuni de backend. Pentru operațiuni și support e util să inspectezi fluxul real de octeți și să îl loghezi în caz de eroare.
Ein sauberer Ansatz: Stream → UTF-8-Decoder → JSON-Parser
Pipeline-ul robust constă din trei etape clare:
- Bytes aus dem Stream lesen (kontrolliert, ggf. mit Limit/Timeout im HTTP-Client).
- Decoding nach Unicode mit explizitem UTF-8 (BOM optional tolerieren).
- Parsing mit System.JSON in ein Objektmodell oder in gezielte Extraktion.
Hebelul cel mai important este etapa 2: Nu vrei ca undeva „Default Encoding“ să decidă. În Delphi asta înseamnă: TEncoding.UTF8 explizit setzen und nicht auf implizite Konvertierungen vertrauen.
Was „schnell“ hier konkret bedeutet
Cu System.JSON nu vei „elimina“ DOM-ul. Dar poți evita:
- o copie suplimentară a întregii Payload ca string intermediar, dacă intern ai nevoie oricum doar de câteva valori (în acest caz este mai potrivit un alt parser; detalii mai jos),
- reîncodări multiple,
- și poți citi controlat payload-uri foarte mari (cu limită de mărime și mesaj de eroare clar), în loc să ajungi la Out-of-Memory sau Access Violations.
Cazul particular: caractere speciale corupte, dar doar uneori
Un caz marginal din practică este deosebit de insidios: Payload-ul este, în principiu, JSON UTF-8 valid, dar îl citești în chunkuri și convertești fiecare chunk la String. Atâta timp cât apare doar ASCII, nu observi nimic. De îndată ce un Umlaut se află exact pe granița unui chunk, apar secvențe UTF-8 invalide. Rezultat: fie caractere corupte, fie o eroare de parsare într-o poziție care nu corespunde conținutului real.
Cum să recunoști asta:
- Erorile de parsare apar „aleator“ la răspunsuri mari, nu la cele mici.
- Același Request funcționează uneori, alteori nu (în funcție de chunking/transport).
- Un hexdump al octeților arată UTF-8 valid, dar stringul înregistrat conține Replacement Characters (�) sau clasicul mojibake.
Soluția nu este să „facă readln mai des“ sau să folosești „buffere mai mari“, ci să utilizezi un decoder care bufferizează corect secvențele multibyte peste granițele chunkurilor. Exact aici TStreamReader în combinație cu o codificare UTF-8 poate fi util — dacă îl inițializezi corect.
Ghid practic: verificarea reproductibilă a UTF-8 în Delphi
Înainte să modifici parserul, ai nevoie de un setup de debugging care îți arate ordinea reală a octeților. Pentru suport și operare este extrem de util, pentru că ulterior vei putea spune clar dacă partea opusă livrează date greșite sau pipeline-ul tău le decodează eronat.
1) Verifică primele octeți (BOM, începutul JSON)
Dacă JSON-ul vine cu BOM, vei vedea la începutul stream-ului EF BB BF. Direct după ar trebui, tipic, să urmeze „{“ sau „[„. Dacă în string apare deja „“, BOM-ul a fost tratat ca text și nu ca BOM.
2) Loghează octeții cruzi în Hex – dar limitat
Nu loga payload-urile complete în producție (protecția datelor, costuri, volum de log). S-au dovedit utile:
- prefixul (de ex. primele 256 sau 1024 octeți),
- suffixul (ultimii 256 octeți),
- și un hash (SHA-256) pentru corelare, când trebuie să compari payload-uri.
Astfel poți diagnostica problemele cu caractere speciale în minute: Este ordinea octeților pentru „ä“ corectă (C3 A4)? Există o truncare? Apar octeți 0x00 neașteptați, de ex. din cauza unei presupuneri greșite de UTF-16?
3) Loghează Content-Type și charset
La HTTP/REST: înregistrează Content-Type și charset declarat. Dacă octeții sunt clar UTF-8, dar charset pretinde altceva, nu ar trebui să urmezi orbește clientul. Pentru JSON, UTF-8 este standardul de facto. În caz de îndoială: analiza pe octeți învinge headerul.
Parser rapid de stream cu System.JSON: design fără copii inutile
Un pattern practic este: citești din stream într-un tampon de octeți, construiești din el un string exact o singură dată folosind UTF-8 și predai acel string parser-ului JSON. Aceasta nu este „streaming” în sensul SAX, dar este un pipeline controlat și performant, fără schimbări surprinzătoare de encoding.
Important pentru arhitectură: construiește funcția astfel încât să decidă central, într-un singur loc:
- Care regulă de encoding se aplică (de regulă UTF-8, BOM opțional)?
- Cât de mare poate fi payload-ul maxim (protecție DoS, prag operațional)?
- Cum arată mesajele de eroare (cu context, dar fără scurgeri de date)?
Strategie de citire: tamponare limitată în loc de „StreamToString“ fără limită
Dacă primești JSON din surse externe (parteneri, clienți mobili, terți), o limită de mărime este obligatorie. Fără limită, o singură solicitare nefericită poate provoca presiune în memorie. Practic înseamnă: la citire, numără suma octeților și oprește-te la atingerea unei limite – cu o excepție clară, care este inteligibilă în sistemul de monitorizare.
De ce adesea mă aștept la UTF-8 „fără BOM”, dar aș tolera BOM-ul
În payload-urile pentru REST BOM apare rar. În schimb, la fișiere (exporturi, editare manuală) apare mai des. Pentru traseele de import robuste este util să tolerezi BOM-ul, dar să îl marchezi vizibil în log, deoarece poate indica „lumea fișierelor” în loc de „lumea API-urilor”.
Ucigașii tăcuți: valorile implicite ale TStreamReader și cititoarele de text în regim mixt
TStreamReader este convenabil, dar trebuie să controlezi corect două aspecte:
- Setează explicit encoding-ul: Nu te baza că îl „recunoaște”.
- Înțelege buffering-ul: Reader-ul face buffering intern. Dacă citești același stream mai târziu în altă parte, poziția contează. Pare trivial, dar devine rapid o sursă de erori în pipeline-urile de import mai mari.
Particular neplăcut este regimul mixt: mai întâi citești o parte ca octeți (de ex. pentru logging sau Magic-Bytes), apoi continui cu TStreamReader. Dacă nu revii corect sau nu inițializezi reader-ul la poziția corectă, vei citi de la octetul 257 în loc de 0. Parser-ul JSON va raporta „Invalid character at position …”, deși payload-ul este corect.
Când caracterele speciale rămân „stricate” în ciuda UTF-8: Escaping vs. Unicode real
JSON poate conține caractere speciale în două moduri:
- Ca caractere UTF-8 reale (de ex. „München” ca octeți C3 BC …).
- Ca secvență de escape (de exemplu „Mu00fcnchen“).
Ambele sunt valide. În practică este important: secvențele de escape ocolesc multe probleme de transport, dar ele doar maschează aparent erorile de encodare. Dacă sistemul tău interpretează greșit octeții undeva, acesta este un risc operațional, nu doar un bug cosmetic. În plus, secvențele de escape pot deruta în jurnalizare/monitorizare dacă echipele se așteaptă să vadă „text lizibil“.
System.JSON îți returnează în ambele cazuri, la final, șiruri Delphi normale (UTF-16), cu condiția ca traseul până acolo să fi fost corect.
Evaluarea realistă a performanței: costurile DOM, array-uri mari și selectivitate
Levierul cel mai mare de performanță nu este adesea „să faci parserul mai rapid“, ci să parsezi mai puțin. Cu System.JSON acest lucru e dificil, pentru că primești DOM-ul. Trei situații tipice:
- Array-uri mari (10.000+ elemente): construirea DOM-ului costă timp și memorie RAM. Dacă ai nevoie doar de 2 câmpuri pe element, un parser capabil de streaming (SAX/Tokenizer) este adesea mai potrivit. System.JSON nu este conceput pentru asta.
- Obiecte individuale cu multe câmpuri: Dacă ai nevoie doar de câteva câmpuri, poți totuși folosi DOM, dar evită traversări multiple. Preia valorile o singură dată și mapează-le în structurile tale.
- Mai multe payload-uri mari consecutiv: În joburi de import sau în worker‑i de sincronizare merită să încadrezi parsarea într-un pas clar și, după fiecare document, să eliberezi toate referințele astfel încât Memory-Managerul să poată curăța. Pare banal, dar în servicii se face adesea „pe lângă”.
Un „stream-parser rapid“ cu System.JSON este astfel adesea un bun compromis: citire eficientă și corectă, utilizare conștientă a DOM-ului, definirea limitelor. Dacă ai nevoie de semantică de streaming reală (de ex. procesarea elementelor dintr-un array unul câte unul, fără a reține totul), System.JSON nu este baza potrivită.
Robustețe în producție: tipuri de eroare și cum să le faci imediat investigabile
Erorile de parsare JSON în jurnale adesea nu sunt utile, pentru că indică doar o poziție. Pentru operare și support ai nevoie de context:
- Poziție pe octeți vs. poziție pe caractere: La UTF-8 acestea nu coincid. Dacă parserul raportează o poziție în caractere, poziția în octeți poate fi diferită. Pentru dump‑uri pe octeți, poziția în octeți este esențială.
- Fragment în jurul erorii: Înregistrează, în caz de eroare, o fereastră mică în jurul poziției (de ex. 40 de caractere înainte/după), dar numai dacă nu conține date sensibile. Alternativ: înregistrează doar în hexazecimal.
- Corelare: Request-ID, Endpoint, Partner-ID, hash-ul payload-ului. Altfel nu vei mai găsi „acea eroare” niciodată.
Scopul este să poți, după un incident în producție, să răspunzi în câteva minute: „Encoding interpretat greșit“, „Payload tăiată“, „Serverul returnează JSON invalid“ sau „avem o problemă de mapping“.
Evitarea capcanelor UTF-8: listă de verificare
- Specifică întotdeauna encoding-ul: La citire din stream și la scriere în jurnale/fișiere nu te baza pe valori implicite.
- Fără conversie chunk‑la‑string: Dacă citești în chunk‑uri, acumulează octeții sau folosește un decoder care bufferizează secvențele multibyte.
- Tolerant la BOM, dar vizibil: Acceptă‑l, dar fii capabil să‑l detectezi în debugging.
- Stabilește limite: dimensiunea maximă a payload-ului, adâncimea maximă a obiectelor/array-urilor (acolo unde o poți controla), timeout‑uri în clientul HTTP.
Când merită efortul pentru un parser de stream cu adevărat?
Nu trebuie să optimizezi fiecare loc în care apare JSON-ul. Abordarea merită de obicei când se aplică cel puțin una din următoarele:
- Payload-uri mari (mai multe MB) apar regulat sau pot apărea.
- Procese de lungă durată (Service, Worker) procesează multe payload-uri și observi vârfuri de memorie sau fragmentare.
- Interop cu sisteme eterogene: mai mulți parteneri, platforme diferite, ocazional codificări eronate.
- Istoric de incidente: au existat deja „diacritice corupte“, erori de parsare sporadice sau întreruperi de import greu de reprodus.
Dacă payload-urile tale sunt mici și provin dintr-o sursă controlată, de cele mai multe ori e suficientă o soluție simplă – dar chiar și atunci: a seta UTF-8 în mod explicit costă aproape nimic și previne surprize ulterioare.
Delimitare: Când ai nevoie de streaming real
System.JSON este orientat pe DOM. Dacă vrei să procesezi datele efectiv „în flux“, de ex. un array mare element cu element, fără a-l reține complet, ai nevoie de o altă abordare de parser (Tokenizer/SAX). Nu este o judecată de valoare, ci o decizie de arhitectură:
- DOM (System.JSON): convenabil, bun pentru obiecte de business tipice, dar consumă multă memorie.
- Streaming/SAX: consum de memorie redus, potrivit pentru date foarte mari, dar necesită mai mult efort de implementare și o gestionare a erorilor mai atentă.
Un compromis rezonabil este adesea: construiește curat gestionarea fluxului, codificarea și limitele, și abia apoi decide dacă DOM mai e potrivit. În multe proiecte, doar asta stabilizează semnificativ operarea.
Concluzie: JSON in Delphi devine fiabil dacă tratezi codificarea și fluxurile ca un strat separat
Majoritatea problemelor legate de „JSON in Delphi“ nu sunt cauzate de parserul JSON în sine, ci de porțiunea discretă dinainte: octeții dintr-un flux sunt transformați în text. Dacă tratezi acolo UTF-8 în mod explicit, detectezi cazurile BOM, nu ignori limitele chunk-urilor și stabilești limite clare de dimensiune, dispar erorile tipice legate de caractere speciale – iar erorile sporadice de parsare devin reproducibile.
System.JSON rămâne un standard pragmatic: nu cel mai rapid streaming-parser, dar solid dacă controlezi inputul și accepți conștient costurile DOM. Dacă dorești, putem parcurge împreună calea ta concretă de import/REST și identifica punctul în care codificarea sau chunking-ul cedează: contactați-ne.
Pentru acest subiect sunt de asemenea importanți parserii JSON de tip stream. Articolul ordonează aceste aspecte într-un mod clar și arată la ce trebuie să fii atent în practică.
Discută un proiect sau o inițiativă de modernizare cu Net-Base.
Pasul următor
Dacă un subiect devine un proiect real, arhitectura, starea existentă și operarea ar trebui analizate împreună încă din faza incipientă.
Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.
- Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
- REST, accesul la date, portalurile și implementarea nu sunt amânate pentru etape ulterioare.
- Veți vedea din timp care opțiune este viabilă din punct de vedere economic și operațional.