GoSuda

Analiza modificărilor de comportament din encoding/json/v2 din Go 1.25 și a stratului de compatibilitate cu v1

By lemonmint views ...

Pachetul encoding/json v2 din Go este o nouă implementare, menită să remedieze mai multe dezavantaje ale versiunii v1 existente (lipsa de consecvență, comportamente surprinzătoare, probleme de performanță). Este o funcționalitate experimentală, activată prin eticheta de compilare goexperiment.jsonv2.

Cel mai important aspect este faptul că atunci când v2 este activat, v1 funcționează ca un strat de compatibilitate care emulează comportamentul v1 peste implementarea v2. Acest lucru se realizează prin intermediul funcției DefaultOptionsV1() din fișierul v2_v1.go. Astfel, v2 oferă opțiuni care permit reproducerea completă a comportamentului v1 și, în același timp, propune noi comportamente implicite mai stricte și mai previzibile.

Principalele obiective ale v2 sunt următoarele:

  1. Îmbunătățirea corectitudinii și a previzibilității: prin aplicarea implicită a unor reguli mai stricte (de exemplu, sensibilitatea la majuscule și minuscule, interzicerea cheilor duplicate), se reduc comportamentele neașteptate.
  2. Îmbunătățirea performanței: motoarele de analiză și de codificare au fost reproiectate pentru a spori eficiența.
  3. Extinderea flexibilității și a controlului: un sistem detaliat de opțiuni permite dezvoltatorilor să controleze în detaliu modul de procesare a JSON.

Diferențe principale de semantică și comportament

Pe baza fișierului v2_test.go, diferențele de comportament dintre v1 și v2 sunt prezentate pe categorii.

1. Potrivirea numelor câmpurilor (sensibilitatea la majuscule și minuscule)

  • Comportamentul v1: la demarshalizarea membrilor unui obiect JSON în câmpurile unei structuri Go, potrivirea se realizează fără a ține cont de majuscule și minuscule (case-insensitive). Atât "FirstName", cât și "firstname" sunt mapate la câmpul FirstName.
  • Comportamentul v2: în mod implicit, potrivirea este sensibilă la majuscule și minuscule (case-sensitive), mapându-se doar câmpurile care corespund exact.
  • Motivul schimbării: potrivirea insensibilă la majuscule și minuscule poate fi o sursă de comportamente neașteptate și poate duce la scăderea performanței atunci când se procesează câmpuri care nu corespund. v2 adoptă ca implicit un comportament mai clar și mai previzibil.
  • Opțiuni asociate: în v2 se poate utiliza eticheta json:"...,case:ignore" pentru a activa explicit, la nivel de câmp, ignorarea majusculelor și minusculelor, sau se poate aplica global opțiunea json.MatchCaseInsensitiveNames(true).

2. Modificarea semanticii opțiunii omitempty

  • Comportamentul v1: câmpul este omis pe baza „stării goale” a valorii Go. Prin „stare goală” se înțelege false, 0, pointerii/interfețele nil, precum și tablourile, slice-urile, map-urile sau șirurile de caractere de lungime 0.
  • Comportamentul v2: câmpul este omis pe baza „stării goale” a valorii JSON codificate. Astfel, câmpul este omis atunci când valoarea se codifică drept null, "", {} sau [].
  • Motivul schimbării: definiția v1 depinde de sistemul de tipuri al Go. v2 se bazează pe sistemul de tipuri JSON, oferind un comportament mai consecvent. De exemplu, în v1 valoarea false a unui câmp de tip bool este omisă, pe când în v2 false este o valoare JSON nevidă și, prin urmare, nu este omisă. v2 introduce opțiunea omitzero, care poate înlocui comportamentul din v1 al omitempty aplicat valorilor 0 sau false.
  • Opțiuni asociate: pentru a obține în v2 același comportament ca în v1, se utilizează opțiunea json.OmitEmptyWithLegacyDefinition(true).

3. Modificarea comportamentului opțiunii de etichetă string

  • Comportamentul v1: se aplică câmpurilor de tip numeric, boolean sau șir de caractere. Valoarea respectivă este recodificată în interiorul unui șir JSON (de exemplu, int(42) -> "42"). Nu se aplică recursiv valorilor din interiorul tipurilor compuse (slice-uri, map-uri etc.).
  • Comportamentul v2: se aplică exclusiv tipurilor numerice și se aplică recursiv. Astfel, și numerele din interiorul unui slice precum []int sunt codificate ca șiruri JSON.
  • Motivul schimbării: scopul principal al opțiunii string este de a preveni pierderea de precizie a întregilor pe 64 de biți, reprezentând numerele ca șiruri de caractere. Comportamentul v1 era limitat și lipsit de consecvență. v2 se concentrează pe această utilizare esențială și extinde comportamentul recursiv, ceea ce o face mai utilă.
  • Opțiuni asociate: comportamentul v1 poate fi imitat cu ajutorul opțiunii json.StringifyWithLegacySemantics(true).

4. Marshalizarea slice-urilor și a map-urilor nil

  • Comportamentul v1: slice-urile nil și map-urile nil sunt marshalizate ca null.
  • Comportamentul v2: în mod implicit, slice-urile nil sunt marshalizate ca [] (tablou gol), iar map-urile nil ca {} (obiect gol).
  • Motivul schimbării: nil este un detaliu de implementare al limbajului Go, iar expunerea acestuia într-un format JSON independent de limbaj nu este de dorit. [] și {} sunt reprezentări mai universale ale colecțiilor goale.
  • Opțiuni asociate: în v2, prin opțiunile json.FormatNilSliceAsNull(true) sau json.FormatNilMapAsNull(true) se poate obține marshalizarea ca null, similar cu v1.

5. Demarshalizarea tablourilor

  • Comportamentul v1: la demarshalizarea într-un tablou Go ([N]T), lungimea tabloului JSON poate diferi de lungimea tabloului Go fără a genera o eroare. Dacă tabloul JSON este mai scurt, spațiul rămas se completează cu valori zero; dacă este mai lung, surplusul este ignorat.
  • Comportamentul v2: lungimea tabloului JSON trebuie să corespundă exact lungimii tabloului Go. În caz contrar, se generează o eroare.
  • Motivul schimbării: în Go, lungimea unui tablou de dimensiune fixă are adesea o semnificație importantă. Comportamentul v1 poate duce la pierderi silențioase de date. v2 aplică reguli mai stricte, sporind corectitudinea.
  • Opțiuni asociate: comportamentul v1 poate fi imitat cu opțiunea json.UnmarshalArrayFromAnyLength(true).

6. Tratarea time.Duration

  • Comportamentul v1: time.Duration este tratat intern ca int64 și este codificat ca un număr JSON, exprimat în nanosecunde.
  • Comportamentul v2: se utilizează metoda time.Duration.String(), iar valoarea este codificată ca șir JSON de forma "1h2m3s".
  • Motivul schimbării: numerele exprimate în nanosecunde sunt greu de citit, iar reprezentarea standard sub formă de șir a time.Duration este mai utilă.
  • Opțiuni asociate: comportamentul v1 poate fi utilizat prin eticheta json:",format:nano" sau prin opțiunea json.FormatTimeWithLegacySemantics(true).

7. Tratarea UTF-8 nevalid

  • Comportamentul v1: la marshalizare/demarshalizare, octeții UTF-8 nevalizi din interiorul unui șir sunt înlocuiți silențios cu caracterul de substituție Unicode (\uFFFD).
  • Comportamentul v2: în mod implicit, la întâlnirea unui UTF-8 nevalid se returnează o eroare.
  • Motivul schimbării: scopul este prevenirea coruperii silențioase a datelor și respectarea mai strictă a standardului JSON (RFC 7493).
  • Opțiuni asociate: comportamentul v1 poate fi imitat cu opțiunea jsontext.AllowInvalidUTF8(true).

8. Tratarea numelor duplicate ale membrilor de obiect

  • Comportamentul v1: sunt permise membri duplicați cu același nume într-un obiect JSON. Valoarea care apare ultima oară suprascrie celelalte.
  • Comportamentul v2: în mod implicit, dacă există nume de membri duplicate, se returnează o eroare.
  • Motivul schimbării: standardul RFC 8259 nu definește comportamentul pentru nume duplicate, astfel încât implementările pot diferi. Acest aspect poate constitui o vulnerabilitate de securitate. v2 respinge explicit astfel de situații, sporind corectitudinea și securitatea.
  • Opțiuni asociate: comportamentul v1 poate fi imitat cu opțiunea jsontext.AllowDuplicateNames(true).

Diferențe de implementare și arhitectură

  • v1: se bazează în mare măsură pe decodeState din decode.go și pe mașina de stări scrisă manual din scanner.go. Este o structură monolitică, în care logica de analiză sintactică și cea de analiză semantică sunt strâns cuplate.
  • v2: arhitectura este mai modulară.
    • encoding/json/jsontext: oferă un tokenizator JSON (Decoder) și un codificator (Encoder) de nivel scăzut și înaltă performanță. Acest pachet se concentrează exclusiv pe aspectele sintactice ale JSON.
    • encoding/json/v2: pe baza jsontext, gestionează conversiile semantice între tipurile Go și valorile JSON.
    • Prin această separare, analiza sintactică și cea semantică sunt disjuncte, ceea ce sporește claritatea codului și performanța.

Noile API-uri și funcționalități ale v2

v2 oferă un control foarte flexibil prin sistemul json.Options.

  • json.Options: un set de opțiuni care modifică comportamentul marshalizării/demarshalizării.
  • json.JoinOptions(...): combină mai multe opțiuni într-una singură.
  • WithMarshalers / WithUnmarshalers: o funcționalitate puternică ce permite injectarea logicii de serializare/deserializare pentru anumite tipuri, fără a implementa interfețele Marshaler/Unmarshaler. Este deosebit de utilă atunci când se lucrează cu tipuri din pachete externe.
  • Opțiuni noi: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull etc., care permit controlul unor comportamente imposibil de realizat în v1.

Concluzie

encoding/json v2 este o implementare modernă, care, pe baza experienței cu v1, îmbunătățește considerabil corectitudinea, performanța și flexibilitatea. Comportamentele implicite au devenit mai stricte, însă, prin sistemul sofisticat de Options, toate comportamentele v1 pot fi reproduse integral, astfel încât compatibilitatea cu codul existent se menține, iar avantajele v2 pot fi adoptate treptat.

  • Pentru proiectele noi, se recomandă utilizarea implicită a v2.
  • Pentru proiectele existente, se poate continua utilizarea jsonv1 ca atare sau se poate migra la jsonv2, introducând treptat comportamentul mai strict al v2 prin intermediul DefaultOptionsV1().