Analisi delle modifiche comportamentali di encoding/json/v2 in Go 1.25 e del livello di compatibilità con v1

Il pacchetto encoding/json v2 di Go è una nuova implementazione volta a migliorare alcuni limiti della v1 (incoerenze, comportamenti sorprendenti, problemi di prestazioni). Essa viene attivata tramite il build tag goexperiment.jsonv2 e costituisce una funzionalità sperimentale.
Il punto più importante è che quando la v2 è attiva, la v1 opera come un livello di compatibilità che emula il comportamento della v1 sopra l'implementazione della v2. Ciò avviene tramite la funzione DefaultOptionsV1() nel file v2_v1.go. In altre parole, la v2 offre opzioni che consentono di riprodurre in modo completo il comportamento della v1, ma propone anche nuovi comportamenti predefiniti più rigorosi e prevedibili.
Gli obiettivi principali della v2 sono i seguenti.
- Miglioramento della correttezza e della prevedibilità: applicare per impostazione predefinita regole più stringenti (ad esempio, distinzione tra maiuscole e minuscole, divieto di chiavi duplicate) per ridurre i comportamenti inattesi.
- Miglioramento delle prestazioni: la progettazione dei motori di parsing e codifica è stata rivista per aumentarne l'efficienza.
- Maggiore flessibilità e controllo: l'introduzione di un sistema di opzioni dettagliato consente agli sviluppatori di controllare nel dettaglio il modo in cui viene gestito il JSON.
Principali differenze di significato e di comportamento
Le differenze di comportamento tra v1 e v2 sono riassunte di seguito per voce, con riferimento principale al file v2_test.go.
1. Corrispondenza dei nomi dei campi (distinzione tra maiuscole e minuscole)
- Comportamento della v1: durante l'unmarshaling di un membro di un oggetto JSON in un campo di una struct Go, la corrispondenza ignora le maiuscole e le minuscole (case-insensitive). Sia
"FirstName"sia"firstname"vengono associati al campoFirstName. - Comportamento della v2: per impostazione predefinita, la corrispondenza distingue le maiuscole dalle minuscole (case-sensitive) e associa solo i campi che corrispondono esattamente.
- Motivazione del cambiamento: la corrispondenza insensibile alle maiuscole può causare comportamenti inattesi e rallentare l'elaborazione dei campi che non corrispondono. La v2 adotta come comportamento predefinito una modalità più chiara e prevedibile.
- Opzioni correlate: nella v2 è possibile attivare esplicitamente l'ignoranza delle maiuscole per singolo campo tramite l'opzione di tag
json:"...,case:ignore", oppure applicarla globalmente con l'opzionejson.MatchCaseInsensitiveNames(true).
2. Modifica del significato dell'opzione di tag omitempty
- Comportamento della v1: il campo viene omesso in base allo "stato vuoto" del valore Go. Per "stato vuoto" si intendono
false,0, i puntatori o le interfaccenil, e gli array, le slice, le mappe e le stringhe di lunghezza zero. - Comportamento della v2: il campo viene omesso in base allo "stato vuoto" del valore JSON codificato. In altre parole, viene omesso quando viene codificato come
null,"",{}o[]. - Motivazione del cambiamento: la definizione della v1 dipende dal sistema di tipi di Go. La v2 si basa sul sistema di tipi JSON, offrendo un comportamento più coerente. Ad esempio, nella v1 il valore
falsedi un campo di tipoboolviene omesso, mentre nella v2falseè un valore JSON non vuoto e quindi non viene omesso. Nella v2 è stata aggiunta l'opzioneomitzero, che può sostituire il comportamento dellaomitemptydella v1 applicato a0ofalse. - Opzioni correlate: per ottenere un comportamento identico a quello della v1 nella v2, si utilizza l'opzione
json.OmitEmptyWithLegacyDefinition(true).
3. Modifica del comportamento dell'opzione di tag string
- Comportamento della v1: si applica ai campi di tipo numerico, booleano e stringa. Il valore viene nuovamente codificato all'interno di una stringa JSON (ad esempio,
int(42)diventa"42"). Non viene applicata ricorsivamente ai valori all'interno di tipi composti (slice, mappe e simili). - Comportamento della v2: si applica solo ai tipi numerici ed è applicata in modo ricorsivo. Ciò significa che anche i numeri all'interno di una slice come
[]intvengono codificati come stringhe JSON. - Motivazione del cambiamento: l'uso principale dell'opzione
stringè rappresentare i numeri a 64 bit come stringhe, per evitare la perdita di precisione. Il comportamento della v1 era limitato e poco coerente. La v2 si concentra su questo uso fondamentale ed estende il comportamento in modo ricorsivo, rendendolo più utile. - Opzioni correlate: l'opzione
json.StringifyWithLegacySemantics(true)permette di imitare il comportamento della v1.
4. Marshaling di slice e mappe nil
- Comportamento della v1: le slice
nile le mappenilvengono marshalate comenull. - Comportamento della v2: per impostazione predefinita, le slice
nilvengono marshalate come[](array vuoto) e le mappenilcome{}(oggetto vuoto). - Motivazione del cambiamento:
nilè un dettaglio implementativo del linguaggio Go, e non è opportuno esporlo in un formato JSON indipendente dal linguaggio. Le notazioni[]e{}, che rappresentano collezioni vuote, sono rappresentazioni più diffuse. - Opzioni correlate: nella v2 è possibile ottenere il marshaling come
null, come nella v1, tramite le opzionijson.FormatNilSliceAsNull(true)ojson.FormatNilMapAsNull(true).
5. Unmarshaling di array
- Comportamento della v1: durante l'unmarshaling in un array Go (
[N]T), se la lunghezza dell'array JSON differisce da quella dell'array Go non viene generato alcun errore. Se l'array JSON è più corto, lo spazio rimanente viene riempito con valori zero; se è più lungo, la parte in eccesso viene scartata. - Comportamento della v2: la lunghezza dell'array JSON deve coincidere esattamente con quella dell'array Go. In caso contrario viene generato un errore.
- Motivazione del cambiamento: in Go, la lunghezza di un array di dimensione fissa ha spesso un significato importante. Il comportamento della v1 può causare perdite silenziose di dati. La v2 aumenta la correttezza con regole più rigorose.
- Opzioni correlate: l'opzione
json.UnmarshalArrayFromAnyLength(true)permette di imitare il comportamento della v1.
6. Gestione di time.Duration
- Comportamento della v1:
time.Durationviene trattato internamente comeint64e codificato come numero JSON in nanosecondi. - Comportamento della v2: viene codificato come stringa JSON in un formato come
"1h2m3s", utilizzando il metodotime.Duration.String(). - Motivazione del cambiamento: i nanosecondi numerici sono poco leggibili, mentre la rappresentazione testuale standard di
time.Durationè più utile. - Opzioni correlate: è possibile utilizzare il comportamento della v1 tramite l'opzione di tag
json:",format:nano"oppure tramite l'opzionejson.FormatTimeWithLegacySemantics(true).
7. Gestione dell'UTF-8 non valido
- Comportamento della v1: durante il marshaling o l'unmarshaling, se una stringa contiene byte UTF-8 non validi, questi vengono sostituiti silenziosamente con il carattere di sostituzione Unicode (
\uFFFD). - Comportamento della v2: per impostazione predefinita, in presenza di UTF-8 non valido viene restituito un errore.
- Motivazione del cambiamento: serve a prevenire la corruzione silenziosa dei dati e a conformarsi allo standard JSON più rigoroso (RFC 7493).
- Opzioni correlate: l'opzione
jsontext.AllowInvalidUTF8(true)permette di imitare il comportamento della v1.
8. Gestione dei nomi di membri duplicati in un oggetto
- Comportamento della v1: un oggetto JSON può contenere membri con lo stesso nome. Viene mantenuto l'ultimo valore, che sovrascrive i precedenti.
- Comportamento della v2: per impostazione predefinita, in presenza di nomi di membri duplicati viene restituito un errore.
- Motivazione del cambiamento: lo standard RFC 8259 non definisce il comportamento in caso di nomi duplicati, quindi le implementazioni possono comportarsi in modo diverso. Ciò può costituire una causa di vulnerabilità di sicurezza. La v2 rifiuta esplicitamente questi casi, aumentando correttezza e sicurezza.
- Opzioni correlate: l'opzione
jsontext.AllowDuplicateNames(true)permette di imitare il comportamento della v1.
Differenze di implementazione e di architettura
- v1: si basa in larga misura su
decodeStateindecode.goe su una macchina a stati scritta a mano inscanner.go. Si tratta di una struttura monolitica in cui la logica di parsing e l'analisi semantica sono fortemente accoppiate. - v2: l'architettura è stata resa più modulare.
encoding/json/jsontext: fornisce un tokenizer JSON (Decoder) e un encoder (Encoder) di basso livello e ad alte prestazioni. Questo pacchetto si concentra esclusivamente sugli aspetti sintattici di JSON.encoding/json/v2: basandosi sujsontext, gestisce la conversione semantica tra tipi Go e valori JSON.- Grazie a questa separazione, l'analisi sintattica e quella semantica sono distinte, con un miglioramento della chiarezza del codice e delle prestazioni.
Nuove API e funzionalità della v2
La v2 offre un controllo molto flessibile tramite il sistema json.Options.
json.Options: è un insieme di opzioni che modificano il comportamento di marshaling e unmarshaling.json.JoinOptions(...): unisce più opzioni in una sola.WithMarshalers/WithUnmarshalers: una funzionalità potente che consente di iniettare la logica di serializzazione e deserializzazione per tipi specifici senza implementare le interfacceMarshaleroUnmarshaler. È particolarmente utile quando si gestiscono tipi di pacchetti esterni.- Nuove opzioni:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNulle altre ancora consentono un controllo del comportamento che in v1 non era possibile.
Conclusioni
encoding/json v2 è un'implementazione moderna che, sulla base dell'esperienza maturata con la v1, migliora in modo significativo correttezza, prestazioni e flessibilità. Il comportamento predefinito è diventato più rigoroso, ma grazie a un sistema di Options articolato è possibile riprodurre integralmente tutti i comportamenti della v1, mantenendo la compatibilità con il codice esistente e adottando gradualmente i vantaggi della v2.
- Per i nuovi progetti, è consigliabile utilizzare la v2 come impostazione predefinita.
- Per i progetti esistenti, è possibile continuare a usare
jsonv1così com'è, oppure eseguire la migrazione ajsonv2introducendo gradualmente le regole più rigorose della v2 tramiteDefaultOptionsV1().