Formattazione, validazione e schema di JSON nella pratica
Riepilogo (TL;DR)
Un team con cui ho lavorato il mese scorso ha rilasciato una modifica di una riga a feature_flags.json. JSON.parse l’ha accettata, la CI era verde, e una volta in staging ogni branch di checkout è tornato al path di codice legacy perché flags.checkout_v2 era la stringa "true" invece del booleano true. “Controllare JSON” era diventato silenziosamente tre attività diverse, e loro ne avevano eseguite solo le prime due. Il pretty-printing riformatta lo spazio bianco così che un umano possa leggere un blob; non verifica nulla. La validazione sintattica conferma che una stringa è JSON parsabile secondo RFC 8259 – parentesi che combaciano, quoting corretto, letterali numerici corretti – ed è esattamente ciò che fa JSON.parse. La validazione strutturale è un passaggio separato che chiede se il valore parsato ha la forma che il tuo programma si aspetta: campi obbligatori, tipi corretti, valori enum consentiti, lunghezze di stringa, intervalli numerici. Quell’ultimo passaggio è esattamente ciò per cui è stato progettato JSON Schema, ed è quello che la maggior parte dei team salta finché un bug di produzione come quello sopra non li costringe ad aggiungerlo. Una pipeline affidabile usa tutti e tre allo strato giusto: formatta quando devi leggere i dati, parsa per catturare input malformati, ed esegui un controllo schema in stile Ajv ai confini di fiducia – richieste API in ingresso, file di configurazione e messaggi tra servizi.
Contesto e concetti
JSON è definito da RFC 8259 (e dall’equivalente ECMA-404). La grammatica è piccola di proposito. Un documento JSON è uno tra: una stringa, un numero, true, false, null, un array o un oggetto. Le stringhe sono tra virgolette doppie e supportano una breve lista di escape tra cui \n, \t, \", \\ e \uXXXX per unità di codice Unicode. I numeri seguono una grammatica decimale con segno, parte frazionaria ed esponente opzionali, ma non c’è distinzione tra intero e virgola mobile a livello di sintassi. Gli oggetti sono collezioni non ordinate di membri con chiave stringa, gli array sono liste ordinate, e lo spazio bianco fuori dalle stringhe è insignificante.
Ciò che JSON intenzionalmente non include è quasi altrettanto importante. Non ci sono commenti, virgole in coda, stringhe con apici singoli, né letterali esadecimali o binari. I caratteri Unicode fuori dall’intervallo ASCII devono apparire come byte UTF-8 grezzi nello stream codificato o come escape \u. Estensioni come JSON5 e HJSON rilassano alcune di queste regole, ma sono formati separati e i parser che accettano JSON stretto li respingeranno.
Una volta parsato un documento, la sua validità sintattica non dice nulla su se sia il dato giusto. Un payload di login come {"user": "x", "pass": "y"} è JSON perfettamente valido, anche se il tuo endpoint si aspettava {"username": "...", "password": "..."}. Per catturare quella classe di errori serve uno schema: una descrizione leggibile dalla macchina di ciò che conta come documento accettabile. JSON Schema (il meta-schema attualmente raccomandato è la Draft 2020-12) colma quella lacuna. Supporta campi obbligatori, vincoli di tipo, enum e const, pattern di stringa via regex, intervalli numerici, items di array e unicità, properties e additionalProperties di oggetti, e composizione via allOf, oneOf, anyOf e $ref. Ajv 8.12 compila uno schema una volta in una funzione validator JavaScript, che è abbastanza veloce per i percorsi caldi – in un servizio Node 20.11 che ho strumentato, un controllo Ajv compilato su un body di richiesta tipico stava nell’ordine delle decine di microsecondi.
Il pretty-printing è il più banale dei tre. Inserisce solo spazio bianco – indentazione e newline – senza cambiare significato. La maggior parte degli editor e degli strumenti da linea di comando lo fa; i devtools del browser lo fanno automaticamente nel pannello di rete. È utile per gli umani e irrilevante per le macchine.
Confronto e dati
| Aspetto | Pretty-printing | Validazione sintattica | Validazione JSON Schema |
|---|---|---|---|
| Scopo | Rendere i dati leggibili | Assicurarsi che il testo parsi come JSON | Assicurarsi che i dati parsati corrispondano a una forma attesa |
| Rileva | Nulla – solo spazio bianco | Parentesi non combacianti, quoting errato, escape non validi, virgole in coda | Campi mancanti, tipi sbagliati, valori fuori intervallo, chiavi sconosciute |
| Non rileva | Problemi strutturali, problemi semantici | Problemi strutturali (forma attesa), regole di business | Regole semantiche oltre lo schema, invarianti tra campi senza keyword custom |
| Tooling tipico | JSON.stringify(obj, null, 2), jq, formatter IDE | JSON.parse, jq -e, qualsiasi parser | Ajv 8.x, python-jsonschema, validator OpenAPI |
| Dove eseguirlo | Strumenti di sviluppo, log | A ogni confine di parsing (implicito) | A richiesta/risposta API, caricamento config, confini di messaggi |
Le tre colonne non sono alternative; sono strati sequenziali. Aprire un package-lock.json di 12 MB su una sola riga in un editor grezzo è come si finisce con una stringa che il tuo strumento di diff si rifiuta di confrontare, e abbellirlo in forma indentata non valida nulla – rende solo la forma leggibile. Quella distinzione conta perché i due strati successivi, sintassi e schema, vengono spesso scambiati per “l’ho pretty-printed e non è esploso nulla”. Pretty-print per leggere, parse per catturare testo malformato, e valida con uno schema per catturare forma sbagliata. Rilasciare solo i primi due è una lacuna comune.
Scenari reali
Scenario 1 — Debug di una risposta API. Un gateway di pagamento di terze parti restituisce un body JSON di 600 righe e il browser lo mostra come una singola riga. Il pretty-printing – sia con devtools del browser, un formatter locale o curl ... | jq . – lo trasforma in qualcosa che un umano può scorrere. Qui non avviene alcuna validazione; lo scopo è la leggibilità mentre cerchi il campo che appare sbagliato, e la disciplina importante è non chiamare questo passaggio “validato”.
Scenario 2 — Caricamento di un file di configurazione. Un servizio legge config.json all’avvio. Un parser JSON rigoroso cattura errori di sintassi come una virgola in coda e si rifiuta di partire, che è il comportamento corretto. Ma, come ha mostrato l’incidente iniziale, un file valido con retries: "three" invece di retries: 3 parsera bene e fallirà solo quando il codice proverà a confrontare una stringa con un numero. Il pattern che ha funzionato per me è mettere Ajv.compile(schema)(config) nelle primissime righe dell’entry point e chiamare process.exit(1) in caso di fallimento – un cambio di cinque minuti che si è ripagato le ultime due volte che qualcuno ha editato il file a mano.
Scenario 3 — Verificare un contratto OpenAPI. Un team rilascia un documento OpenAPI 3.1 che descrive endpoint, body di richiesta e forme di risposta sotto components.schemas. I test di contratto prendono i payload di esempio dalla spec e li validano contro gli schemi usando un validator JSON Schema. Quando un’implementazione server deriva – per esempio, inizia a restituire un intero dove la spec prometteva una stringa – il test di contratto segnala la discrepanza prima che un client si rompa in produzione. Questo è lo stesso motore JSON Schema che useresti per un singolo file di config; la differenza è la scala di copertura.
Errori comuni
“JSON.parse basta per validare JSON.” Valida la sintassi, non la forma. Un parser restituirà felicemente un oggetto a cui manca metà dei campi di cui il tuo codice ha bisogno. Tratta JSON.parse come un varco contro testo malformato e sovrapponi un controllo schema quando i dati attraversano un confine di fiducia.
“JSON Schema è solo lato server.” Validare nel browser prima di inviare una richiesta dà agli utenti feedback istantaneo e taglia carico sul server. Molte librerie di form e Ajv stesso girano comodamente nel browser. La validazione lato server deve comunque girare – mai fidarsi del client – ma i controlli lato client migliorano la UX senza indebolire il modello di sicurezza.
“JSON5 è solo JSON con commenti.” JSON5 aggiunge commenti, chiavi senza virgolette, virgole in coda, numeri esadecimali e altro. Questo lo rende più amichevole come formato di config editato dall’uomo – il consumatore più noto è tsconfig.json, che in realtà usa JSONC (un superset non standard diverso) – ma tutto ciò che segue strettamente RFC 8259 non lo accetterà. Usa JSON5/JSONC dove il consumatore documenta il supporto; emetti JSON stretto quando scrivi sulla rete o verso qualsiasi strumento il cui parser non controlli.
“YAML è solo JSON con indentazione.” Ogni documento JSON valido è YAML valido, ma YAML aggiunge feature – ancore e alias, tipi taggati, più documenti in un file, scalari a blocco e piegati, parsing sensibile all’indentazione – che introducono bug che JSON non può avere. Il classico è il cosiddetto “Norway problem”: YAML 1.1 interpreta NO come il booleano false a meno che tu lo quoti. Passare da JSON a YAML per “renderlo leggibile” scambia una classe di problemi con un’altra.
Lista di controllo
- Ti serve solo leggere i dati? Pretty-printali. Non affermare che sono “validati”.
- Stanno attraversando un confine di fiducia (richiesta HTTP, coda di messaggi, file di config)? Parsa e poi esegui un controllo JSON Schema, non solo un parse.
- Ti importa dei campi sconosciuti? Imposta
additionalProperties: falsenello schema e decidi se respingere o rimuovere gli extra. - Gli errori sono actionable? Configura il validator (per esempio, Ajv con
allErrors: true) per restituire ogni violazione così che gli utenti vedano tutti i problemi in una volta. - Lo schema vive accanto al codice che usa i dati? La deriva tra una spec e un’implementazione è più facile da prevenire quando lo schema è una fonte di verità per entrambi.
- Il formato è abbastanza stabile per JSON? Se gli umani devono editarlo e controlli tu il parser, JSON5/JSONC o TOML possono essere più permissivi. Se lo scambiano macchine, resta con JSON stretto.
Strumento correlato
Il formatter JSON di Patrache Studio gira nel browser, così il payload che incolli non lascia mai la tua macchina – utile quando ispezioni una risposta di produzione che contiene dati personali. I payload raramente vivono da soli: se il JSON che stai formattando include un binario embedded, le regole in Base64 e codifica URL: scopo, insidie, uso corretto spiegano perché il blob si espande e quando un trasporto diverso è appropriato. Se il payload include ID, UUID v1 vs v4 vs v7: scegliere una chiave primaria DB copre perché la versione esatta che generi sul server influisce sul modo in cui i sistemi downstream indicizzano, ordinano e mettono in cache quel JSON.
Riferimenti
- IETF RFC 8259, “The JavaScript Object Notation (JSON) Data Interchange Format” — https://datatracker.ietf.org/doc/html/rfc8259
- JSON Schema Specification (Draft 2020-12) — https://json-schema.org/specification
- Ajv — Another JSON Schema Validator — https://ajv.js.org/
- MDN, “Working with JSON” — https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON