GoSuda

Go 1.25 encoding/json v1 vs v2 Confronto

By lemonmint
views ...

La v2 del pacchetto encoding/json di Go è una nuova implementazione volta a migliorare le numerose lacune della v1 (mancanza di coerenza, comportamenti sorprendenti, problemi di performance). Si tratta di una funzionalità sperimentale che viene abilitata tramite il tag di build goexperiment.jsonv2.

L'aspetto più rilevante è che quando la v2 è abilitata, la v1 opera come uno strato di compatibilità che emula il comportamento della v1 sopra l'implementazione v2. Ciò avviene tramite la funzione DefaultOptionsV1() nel file v2_v1.go. In altre parole, la v2 offre opzioni che consentono di riprodurre perfettamente il comportamento della v1, proponendo contestualmente nuovi comportamenti predefiniti più rigorosi e prevedibili.

Gli obiettivi principali della v2 sono i seguenti:

  1. Miglioramento di Accuratezza e Prevedibilità: Riduce i comportamenti inattesi applicando di default regole più stringenti (ad esempio, distinzione tra maiuscole e minuscole, divieto di chiavi duplicate).
  2. Miglioramento delle Performance: Ha aumentato l'efficienza riprogettando i motori di parsing e di encoding.
  3. Estensione di Flessibilità e Controllo: Ha introdotto un sistema di opzioni dettagliato che permette agli sviluppatori di regolare finemente la modalità di gestione di JSON.

Principali Differenze Semantiche/Comportamentali

Abbiamo riepilogato le differenze di comportamento tra v1 e v2, punto per punto, concentrandoci sul file v2_test.go.

1. Corrispondenza dei Nomi dei Campi (Case Sensitivity)

  • Comportamento v1: Durante l'unmarshalling dei membri di un oggetto JSON in campi di una struct Go, la corrispondenza avviene ignorando le maiuscole/minuscole (case-insensitive). Sia "FirstName" che "firstname" vengono mappati al campo FirstName.
  • Comportamento v2: Di default, la corrispondenza è sensibile alle maiuscole/minuscole (case-sensitive) e mappa solo i campi che corrispondono esattamente.
  • Motivo della Modifica: La corrispondenza case-insensitive può essere causa di comportamenti imprevisti e può indurre una riduzione delle performance durante la gestione di campi non corrispondenti. La v2 adotta di default un comportamento più chiaro e prevedibile.
  • Opzioni Correlate: Nella v2, è possibile abilitare esplicitamente l'ignoranza delle maiuscole/minuscole per campo utilizzando l'opzione di tag json:"...,case:ignore", oppure applicare globalmente l'opzione json.MatchCaseInsensitiveNames(true).

2. Modifica del Significato dell'Opzione di Tag omitempty

  • Comportamento v1: Omette il campo basandosi sullo "stato vuoto (empty)" del valore Go. Per "stato vuoto" si intendono false, 0, puntatori/interfacce nil, e array/slice/map/stringhe di lunghezza zero.
  • Comportamento v2: Omette il campo basandosi sullo "stato vuoto" del valore JSON codificato. Ciò significa che viene omesso se codificato come null, "", {}, o [].
  • Motivo della Modifica: La definizione della v1 dipende dal sistema di tipi di Go. La v2 fornisce un comportamento più coerente basato sul sistema di tipi JSON. Ad esempio, in v1 il valore false di un tipo bool viene omesso, mentre in v2 false è un valore JSON non vuoto e quindi non viene omesso. Nella v2 è stata aggiunta l'opzione omitzero per sostituire il comportamento della v1 in cui omitempty veniva applicato a 0 o false.
  • Opzioni Correlate: Se si desidera lo stesso comportamento della v1 nella v2, si utilizzi l'opzione json.OmitEmptyWithLegacyDefinition(true).

3. Modifica del Comportamento dell'Opzione di Tag string

  • Comportamento v1: Si applica ai campi di tipo numerico, booleano e stringa. Ricodifica il valore all'interno di una stringa JSON (ad esempio, int(42) -> "42"). Non si applica ricorsivamente ai valori interni di tipi compositi (slice, map, ecc.).
  • Comportamento v2: Si applica solo ai tipi numerici e si applica in modo ricorsivo. Ciò significa che anche i numeri all'interno di slice come []int vengono tutti codificati come stringhe JSON.
  • Motivo della Modifica: Lo scopo principale dell'opzione string è rappresentare i numeri come stringhe per prevenire la perdita di precisione per gli interi a 64 bit. Il comportamento della v1 era limitato e carente di coerenza. La v2 si concentra su questo uso fondamentale ed estende il comportamento in modo ricorsivo per renderlo più utile.
  • Opzioni Correlate: È possibile simulare il comportamento della v1 con l'opzione json.StringifyWithLegacySemantics(true).

4. Marshalling di Slice e Map nil

  • Comportamento v1: Le slice nil e le map nil vengono marshallate come null.
  • Comportamento v2: Di default, le slice nil vengono marshallate come [] (array vuoto) e le map nil come {} (oggetto vuoto).
  • Motivo della Modifica: nil è un dettaglio implementativo del linguaggio Go, ed è indesiderabile esporlo nel formato JSON, che è indipendente dal linguaggio. [] o {} sono rappresentazioni più universali per indicare una collezione vuota.
  • Opzioni Correlate: Nella v2, è possibile effettuare il marshalling come null, similmente alla v1, tramite le opzioni json.FormatNilSliceAsNull(true) o json.FormatNilMapAsNull(true).

5. Unmarshalling di Array

  • Comportamento v1: Durante l'unmarshalling in un array Go ([N]T), non viene generato un errore se la lunghezza dell'array JSON è diversa dalla lunghezza dell'array Go. Se la lunghezza è inferiore, lo spazio rimanente viene riempito con valori zero; se è superiore, la parte in eccesso viene scartata.
  • Comportamento v2: La lunghezza dell'array JSON deve corrispondere esattamente alla lunghezza dell'array Go. In caso contrario, viene generato un errore.
  • Motivo della Modifica: Negli array a dimensione fissa di Go, la lunghezza spesso riveste un significato importante. Il comportamento della v1 può portare a una perdita silente di dati. La v2 aumenta l'accuratezza con regole più rigorose.
  • Opzioni Correlate: È possibile simulare il comportamento della v1 con l'opzione json.UnmarshalArrayFromAnyLength(true).

6. Gestione di time.Duration

  • Comportamento v1: time.Duration viene trattato internamente come int64 e codificato come un numero JSON in nanosecondi.
  • Comportamento v2: Viene codificato come una stringa JSON nel formato "1h2m3s", utilizzando il metodo time.Duration.String().
  • Motivo della Modifica: I nanosecondi numerici sono meno leggibili, mentre la rappresentazione stringa standard di time.Duration è più utile.
  • Opzioni Correlate: È possibile utilizzare il comportamento della v1 tramite l'opzione di tag json:",format:nano" o l'opzione json.FormatTimeWithLegacySemantics(true).

7. Gestione degli UTF-8 Non Validi

  • Comportamento v1: Durante il marshalling/unmarshalling, se sono presenti byte UTF-8 non validi all'interno di una stringa, questi vengono sostituiti silenziosamente con il carattere di sostituzione Unicode (\uFFFD).
  • Comportamento v2: Di default, se viene incontrato un UTF-8 non valido, viene restituito un errore.
  • Motivo della Modifica: Ciò serve a prevenire la corruzione silente dei dati e ad aderire allo standard JSON più rigoroso (RFC 7493).
  • Opzioni Correlate: È possibile simulare il comportamento della v1 tramite l'opzione jsontext.AllowInvalidUTF8(true).

8. Gestione dei Nomi di Membro di Oggetto Duplicati

  • Comportamento v1: Consente la presenza di membri con lo stesso nome duplicati all'interno di un oggetto JSON. Il valore che appare per ultimo sovrascrive gli altri.
  • Comportamento v2: Di default, se sono presenti nomi di membro duplicati, viene restituito un errore.
  • Motivo della Modifica: Lo standard RFC 8259 non definisce il comportamento dei nomi duplicati, il che può portare a comportamenti divergenti tra le implementazioni. Questo può essere fonte di vulnerabilità di sicurezza. La v2 aumenta l'accuratezza e la sicurezza rifiutando esplicitamente tale situazione.
  • Opzioni Correlate: È possibile simulare il comportamento della v1 con l'opzione jsontext.AllowDuplicateNames(true).

Differenze di Implementazione e Architettura

  • v1: Dipende fortemente da decodeState in decode.go e dalla macchina a stati (state machine) scritta manualmente in scanner.go. Questa costituisce una struttura monolitica in cui la logica di parsing e l'analisi semantica sono strettamente accoppiate.
  • v2: L'architettura è più modulare.
    • encoding/json/jsontext: Fornisce un tokenizer JSON ad alte prestazioni di basso livello (Decoder) e un encoder (Encoder). Questo pacchetto si concentra esclusivamente sugli aspetti sintattici di JSON.
    • encoding/json/v2: Gestisce la conversione semantica tra i tipi Go e i valori JSON, basandosi su jsontext.
    • Questa separazione disaccoppia l'analisi sintattica dall'analisi semantica, migliorando la chiarezza del codice e le performance.

Nuove API e Funzionalità di v2

La v2 offre funzionalità di controllo estremamente flessibili tramite il sistema json.Options.

  • json.Options: È un insieme di opzioni che modificano il comportamento di marshalling/unmarshalling.
  • json.JoinOptions(...): Unisce più opzioni in una singola entità.
  • WithMarshalers / WithUnmarshalers: È una potente funzionalità che consente di iniettare logiche di serializzazione/deserializzazione per tipi specifici senza che essi debbano implementare le interfacce Marshaler/Unmarshaler. Ciò è particolarmente utile quando si gestiscono tipi provenienti da pacchetti esterni.
  • Nuove Opzioni: È divenuto possibile controllare svariati comportamenti impossibili nella v1, come RejectUnknownMembers, Deterministic(false), e FormatNilSliceAsNull.

Conclusione

La v2 di encoding/json è un'implementazione moderna che migliora significativamente accuratezza, performance e flessibilità, basandosi sull'esperienza maturata con la v1. Sebbene il comportamento predefinito sia divenuto più rigoroso, essa supporta pienamente tutte le funzionalità della v1 attraverso il sofisticato sistema di Options, consentendo l'introduzione graduale dei vantaggi della v2 mantenendo la compatibilità con il codice esistente.

  • Per i nuovi progetti, è consigliabile utilizzare la v2 come impostazione predefinita.
  • I progetti esistenti possono continuare a utilizzare jsonv1 come è, oppure possono adottare una strategia di migrazione a jsonv2 introducendo gradualmente i comportamenti rigorosi della v2 tramite DefaultOptionsV1().