Stesso JSON, quattro strumenti, quattro risultati: come scegliere un formattatore
In sintesi: La formattazione non è senza perdita. Lo stesso JSON con zero iniziale, BOM o intero di 20 cifre dà risultati diversi con Python, Node, jq e json.tool: alcuni segnalano un errore, altri modificano il valore in silenzio. Dieci campioni eseguiti in locale su quattro percorsi producono una tabella di scelta e uno script di validazione rigorosa.

Conclusione: la differenza decisiva tra formattatori JSON non è quanti spazi indentano, ma che la stessa ingresso qui solleva un errore e là riscrive i dati in silenzio. Un solo campione con valori limite insegna più di qualsiasi elenco di funzioni.
Si crede che formattare sia solo «risistemare il testo». In realtà ogni percorso lo reinterpreta con il proprio analizzatore. Se l'analizzatore è tollerante, l'errore che dovrebbe comparire diventa una modifica silenziosa del valore, mentre la pagina continua a mostrare un verde rassicurante «Formattazione riuscita».
1. Il fatto: un ingresso, quattro percorsi, quattro risultati
Prova con Python 3.11.1, Node v22.13.1 e jq 1.7. Dieci brevi stringhe JSON costruite a mano sono state inviate dallo standard input, senza scrivere su disco né cambiare configurazione. Percorsi: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR indica eccezione e arresto; SILENT indica nessun errore ma uscita diversa dall'ingresso.
| Campione | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK fedele | SILENT 9007199254740992 | OK fedele | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
Quattro campioni su dieci hanno dato «nessun errore ma dati cambiati»: S4, S6, S7, S8. Chi si fida solo della luce verde ha il quaranta per cento di probabilità di tenere qualcosa di diverso dall'originale. Il peggio è che nessuno di questi casi compare in un elenco di errori di sintassi: il file si apre, si copia, si esporta; il guasto è nel contenuto, non nella sintassi.
2. La causa: sintassi e semantica sono due livelli
La RFC 8259 è esplicita: la sezione 1.2 raccomanda di evitare, per interoperabilità, pratiche che ECMA-404 consente; la sezione 4 richiede nomi unici solo come SHOULD senza definire il trattamento delle chiavi duplicate; la sezione 6 dà solo indicazioni sui numeri. ECMA-404 va oltre e si dichiara «agnostica rispetto alla semantica dei numeri»: specifica la rappresentazione, non la memorizzazione. La norma lascia dunque un'ampia zona di confine a chi implementa.
Così il «formattare» si divide: il livello sintattico decide se il testo è legale, quello semantico decide quale valore diventa. Il primo ha una risposta standard, il secondo no. jq che legge 01 come 1 e salta un BOM iniziale è una scelta semantica; Node che spinge un intero di 20 cifre in un double IEEE 754 è una scelta del runtime; i quattro percorsi che tengono l'ultimo valore di {"a":1,"a":2} è una convenzione condivisa. Niente di tutto ciò viola «analizzabile», ma tutto cambia i dati.
Come correggere gli errori di parsing JSON lavora sul livello sintattico e traduce il messaggio d'errore in coordinate. Questo articolo tratta il passo prima: se lo strumento non segnala alcun errore, non ottieni nemmeno le coordinate.
3. Quattro riscritture silenziose, una per una
Isolati, i quattro campioni mostrano modifiche così piccole da sfuggire a una scorsa rapida:
{"a": 01} // zero iniziale: jq restituisce {"a":1}
\ufeff{"a": 1} // BOM: jq salta i primi tre byte
{"id": 9007199254740993} // intero di 20 cifre: JSON.parse dà ...992
{"a": 1, "a": 2} // chiave duplicata: i quattro tengono {"a":2}
Zero iniziale e BOM sono del tipo «l'ingresso era già invalido, ma lo strumento è stato tollerante». 01 è illegale nel JSON standard (RFC 8259 §6) e un BOM non deve aprire un testo JSON; Python e Node rifiutano, jq prosegue. Comodo per pulire i log, disastroso nella validazione di contratto: a monte si crede di aver inviato dati invalidi da correggere, a valle si ha già un risultato «dall'aspetto normale».
L'intero grande è «ingresso perfettamente legale, ma il runtime non lo contiene». 9007199254740993 è un numero JSON legale, ma oltre 2^53-1 JSON.parse può rappresentare solo 9007199254740992. L'int di Python e jq conservano il valore. Se è un numero d'ordine o uno Snowflake ID, un'ultima cifra cambiata squaglia la riconciliazione.
Le chiavi duplicate sono «la norma non dice, ogni implementazione sceglie». Tutti e quattro i percorsi prendono in silenzio l'ultimo valore, senza avviso. La RFC 8259 dice solo SHOULD, quindi non è un bug, ma è la classe di problemi più difficile da rintracciare concatenando configurazioni o unendo patch.
4. Segnalazione errori: chi dà la posizione, chi una riga sola
Anche nei sei campioni falliti l'informazione varia molto. La JSONDecodeError di Python espone msg, lineno, colno e pos; Node v22 riporta position N (line 1 column M) con un frammento del testo; python -m json.tool riutilizza i dati di Python; jq dà solo parse error: ... at line N, senza offset di carattere.
Su JSON minificato in una riga, riga e colonna sono quasi inutili: tutto è sulla riga 1. Serve un offset di carattere. Questo script uniforma la validazione in «errore + offset + contesto»:
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
È più rigoroso di jq . (rifiuta zero iniziale e BOM) e fornisce e.pos. Come convertire line 1 column 23611 in offset di byte e perché offset di caratteri e di byte differiscono di tre volte nei documenti CJK è spiegato in Debug di JSON minificato; non si ripete qui.
5. La correzione: separare «validazione rigorosa» e «abbellimento»
Conclusione semplice: un solo pulsante «Formatta» non deve portare insieme validazione e abbellimento. Prima un analizzatore rigoroso decide la legalità, poi si sceglie la presentazione. Una riga isola il primo passo:
python3 -m json.tool input.json > /dev/null && echo "sintassi OK" || echo "sintassi non valida"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
La prima risponde solo «è analizzabile?», la seconda verifica «la struttura è quella attesa?». Con interi grandi leggere con Python o jq invece di JSON.parse; con possibile BOM aprire esplicitamente con encoding='utf-8-sig' invece di sperare in un salto automatico; con chiavi possibilmente duplicate scansionare prima dell'analisi, perché nessuno dei quattro percorsi avvisa. Portato in CI è esattamente la pipeline di Editor, riga di comando e toolchain di automazione con uno strato in più di asserzioni di fedeltà.
6. Come scegliere: tabella dei quattro percorsi
| Percorso | Punto forte | Rischio | Ideale per |
|---|---|---|---|
python json.loads / json.tool |
Rigoroso, dà riga/colonna e offset, interi senza perdita | Rifiuta zero iniziale e BOM, codifica da gestire | Validazione di contratto, CI, analisi log |
node JSON.parse |
Nativo nel browser, errore con frammento di sorgente | Interi grandi troncati in silenzio, errore su BOM | Debug front, strumenti in browser |
jq -c . |
Amico delle pipe, risultato più rapido | Zero iniziale e BOM passano in silenzio, senza offset | Pipeline operative, estrazione rapida |
Strumenti browser /parse, /editor |
Vista ad albero, ordina chiavi, copia percorso, senza installazione | Dipende dall'analizzatore della pagina, va verificato una volta | Ispezione manuale, diagnosi estemporanea |
La domanda non è «chi ha più funzioni» ma «mentirà sui miei dati?». Con ID d'ordine, importi o ID dispositivo meglio Python o uno strumento che dichiari la fedeltà; per puro debug di struttura senza persistenza basta il browser. Il /parse di questo sito valida e formatta nel browser, ideale per «vedere com'è fatto», ma con interi grandi ricontrolla con lo script sopra.
7. La privacy è solo uno dei confini
«I miei dati verranno caricati?» è la domanda più frequente sugli strumenti online, ma è indipendente da «i miei dati verranno modificati?». Uno strumento locale offline può troncare comunque un intero di 20 cifre; viceversa uno strumento online che segnala rigorosamente gli errori può fare POST del testo originale. Le due proprietà si verificano separatamente e non si sostituiscono. La FAQ privacy dà tre controlli da due minuti; questo articolo aggiunge l'altra metà: con l'elenco di campioni sotto, confermare che lo strumento non riscriva valori in silenzio.
8. Lista di autoverifica copiabile
Salva S1–S10 in cases.txt, una riga per campione, incolla in ogni candidato e verifica tre cose: i campioni non validi falliscono davvero, il fallimento dà una posizione concreta, l'uscita di un campione valido coincide carattere per carattere con l'ingresso. Controlla soprattutto S4, S6, S7, S8: una sola riga SILENT significa che quel percorso non può validare dati da solo.
Aggiungi tre vincoli ingegneristici: mettere la validazione rigorosa in un hook pre-commit invece di scoprire lo squilibrio in produzione; scrivere i file come UTF-8 senza BOM in tutte le lingue; per JSON di terzi conservare il corpo di risposta grezzo e usare il risultato formattato solo per leggere, mai per riscrivere.
Domande frequenti
Una formattazione riuscita significa che il JSON è a posto?
No. S4, S6, S7 e S8 hanno tutti «passato» la formattazione, ma zero iniziale, BOM, intero di 20 cifre e chiave duplicata sono stati riscritti o inghiottiti. Formattare prova solo che qualche analizzatore ha accettato il testo, non che i valori eguaglino quelli inviati. Verifica i campi critici valore per valore.
Perché jq accetta zero iniziale e BOM che Python rifiuta?
jq ha un proprio analizzatore lessicale: è tollerante con numeri come 01 e salta un BOM iniziale, mentre Python e Node seguono strettamente la RFC 8259. Non è un bug di versione, è il compromesso tra analizzatore tollerante e rigoroso. Il primo fa risparmiare lavoro nella pulizia, il secondo è obbligatorio nella validazione di contratto.
Come evitare che un intero grande diventi 9007199254740992?
Il Number di JavaScript è un double IEEE 754 con limite sicuro 2^53-1. Leggere un ID di 20 cifre con JSON.parse lo tronca inevitabilmente. Salva questi ID come stringa, o usa l'int di Python, jq o un analizzatore con BigInt. Se il front serve davvero un numero, sostituisci il campo con una stringa prima dell'analisi.
Strumento online o riga di comando locale?
Per sensibilità e scopo: per vedere la struttura o trovare un campo il browser è il più rapido; per pipeline e asserzioni di fedeltà python -m json.tool più un verificatore rigoroso. In entrambi i casi esegui prima S1–S10, conferma errori e uscite, poi decidi quale classe di dati inserire.