Quando due documenti JSON differiscono: diff posizionale, diff per id o normalizza prima
In sintesi: Cinquanta record diventano cinquantuno e il diff segnala cinquantuno modifiche. Copre i falsi positivi del diff testuale, il matching degli array per id, la normalizzazione con jq e le patch applicabili.

In sintesi: normalizza prima, poi confronta — usa jq -S o una serializzazione stabile per cancellare ordine delle chiavi, indentazione e grafia dei numeri, ordina o indicizza gli array per una chiave di business quando l'ordine è privo di significato, e solo allora trasforma il diff in una patch.
Una differenza JSON non è una differenza di testo. Confronta i file come stringhe e un riordino delle chiavi illumina l'intera pagina; confronta gli array per indice e inserire un record segna gli altri cinquanta come modificati.
Una patch spedita al servizio ordini, e tutto ciò che hai sono due risposte: cinquanta record prima, cinquantuno dopo, con un nuovo record inserito nel mezzo. Lo strumento diff segnala cinquantuno modifiche, un muro di rosso, e continua a non riuscire a capire quale record si è davvero spostato.
Lo strumento non è rotto. "Confrontare" significa tre cose diverse su JSON, e la maggior parte delle persone raggiunge per impostazione predefinita la meno utile.
Riprodurre il falso allarme: un inserimento, cinquantuno modifiche
Trenta righe di Node 22: costruisci cinquanta ordini ({id, sku, qty}), poi aggiungine uno in coda e inseriscine uno in testa, contando le differenze con allineamento indice per indice.
| Operazione | Record | Modifiche segnalate per posizione |
|---|---|---|
| Aggiungi uno in coda | 50 → 51 | 1 |
| Inserisci uno in testa | 50 → 51 | 51 |
Stesso "un record in più", cinquantuno volte il rumore. La ragione è banale: quando confronti per indice, il nuovo elemento atterra alla posizione 0 e ogni record esistente si sposta di uno slot. Posizione 0 versus posizione 0, posizione 1 versus posizione 1 — nessuna coppia è uguale.
Costruisci invece una Map con chiave id e gli stessi dati segnalano esattamente una voce: $[id=ORD-999] added.
Quell'esperimento ti dà la regola che conta più di qualsiasi raccomandazione di strumenti: se l'array è una sequenza ordinata o un insieme non ordinato decide quale confronto dovresti usare.
Tre modi in cui un diff testuale mente su JSON
diff a.json b.json confronta caratteri. JSON porta una struttura dati. Questo disallineamento produce tre affidabili tipi di falsi positivi.
1. Chiavi degli oggetti riordinate
La sezione 4 di RFC 8259 è esplicita: un oggetto è "una collezione non ordinata di coppie nome/valore". Questi due documenti sono lo stesso valore:
{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }
Un diff di righe dice che tutte e tre le righe sono cambiate. Ho invertito l'ordine delle chiavi di un oggetto da dieci chiavi, stampato entrambi in modo leggibile e confrontato riga per riga: tutte e dieci le righe segnalate, mentre un confronto strutturale dice che sono uguali. Rapporto di rumore 10:0.
Perché l'ordine delle chiavi cambia affatto? Cambiare serializzatore, eseguire un formattatore con chiavi ordinate, ricostruire l'oggetto da un dizionario o hash map, passare tra runtime di linguaggi diversi — uno qualsiasi di questi basta per far sembrare diversi dati identici su disco.
2. Spazi e indentazione
{"a":1} e {\n "a": 1\n} si analizzano nello stesso valore. Un diff testuale vede newline e spazi e segnala tutto.
3. Grafie diverse di numeri ed escape
1.0 e 1, 1e2 e 100, "\u00e9" e "é" — stesso valore dopo il parsing, testo diverso dopo la serializzazione. Questo è più mite perché tocca poche righe, ma nelle revisioni di configurazione basta a farti pensare che un numero sia cambiato.
Il caso inverso è ciò che il testo non può vedere e la struttura sì: 5 versus "5". Una coppia di virgolette in più, ma il tipo è andato alla deriva — di solito un bug piuttosto che un aggiornamento. Un confronto strutturale lo segnala come modify senza esitare.
Tre modalità di confronto, tre compiti
| Modalità | Cosa viene confrontato | Da dove viene il rumore | Output | Si adatta a |
|---|---|---|---|---|
| Diff testuale | Righe di caratteri | Ordine delle chiavi, indentazione, grafia dei numeri | Numeri di riga, rosso/verde | Bundle di localizzazione, fixture con ordine delle chiavi stabile |
| Diff strutturale | Alberi analizzati, allineati per percorso | Array allineati per indice | Percorsi con add/remove/modify | Risposte API, configurazioni, webhook |
| Diff semantico | Alberi più chiavi di business | Solo una cattiva scelta della chiave | Percorsi con rilevamento di move/insert | Liste di record, ordini, inventario |
Il diff strutturale risolve lo strato "ordine delle chiavi e formattazione". Il diff semantico va oltre e risolve "identità degli elementi dell'array". La differenza non è la complessità implementativa; è se sei disposto a dire allo strumento quale campo è il passaporto di un record.
Gli array sono la linea di demarcazione
Fai una domanda: l'ordine di questo array è esso stesso un dato?
- L'ordine è un dato: passi di migrazione, classifiche di ricerca, punti di un grafico, catene di approvazione. Un cambio di posizione è un cambio di comportamento, quindi il confronto posizionale è corretto.
- L'ordine è privo di significato: tag, insiemi di ID di permessi, righe di un ordine, liste di inventario. Un inserimento non dovrebbe influire su come vengono giudicati gli altri elementi.
Quando l'ordine è privo di significato, indicizza i record per una chiave invece di martellarli per posizione:
// Match by id: an insert or delete reports one entry, other items stay put
function diffById(before, after, path = '$') {
const out = [];
const beforeMap = new Map(before.map((item) => [item.id, item]));
const afterMap = new Map(after.map((item) => [item.id, item]));
for (const [id, value] of afterMap) {
if (!beforeMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'added' });
}
for (const [id, value] of beforeMap) {
if (!afterMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'removed' });
else if (JSON.stringify(value) !== JSON.stringify(afterMap.get(id))) {
out.push({ path: `${path}[id=${id}]`, type: 'changed' });
}
}
return out;
}
Le librerie mature trasformano questo in un'opzione. jsondiffpatch usa un LCS per trovare la più lunga sottosequenza comune, ma gli elementi oggetto dentro gli array sono abbinati per posizione per impostazione predefinita — la documentazione ufficiale sottolinea che devi fornire una funzione objectHash per abbinarli per contenuto. Gli utenti alle prime armi spesso concludono che la libreria sia stupida; in realtà non hanno mai passato una chiave.
Anche la scelta della chiave conta. id è la scelta sicura. sku funziona solo quando gli SKU sono univoci. Hasare l'intero oggetto trasforma "un campo è cambiato" in "uno rimosso più uno aggiunto", il che è peggio del confronto per posizione.
Il trucco da terminale più veloce: normalizzare con jq
Se non vuoi una libreria e ti serve solo dare un'occhiata, normalizza prima e fai il diff dopo. jq -S ordina le chiavi degli oggetti ricorsivamente e applica un'indentazione coerente:
# Normalize, then compare line by line; key order and indentation noise disappear
diff -u <(jq -S . before.json) <(jq -S . after.json)
# When the array is genuinely unordered, sort by key before comparing
diff -u <(jq -S '.items |= sort_by(.id)' before.json) \
<(jq -S '.items |= sort_by(.id)' after.json)
Ho eseguito printf '{"b":2,"a":1,"arr":[1,2]}' | jq -S . e ho ottenuto {"a":1,"arr":[1,2],"b":2} — chiavi ordinate, arr intatto. Ricorda questo: -S ordina solo le chiavi degli oggetti, mai gli elementi degli array. O ordina gli array esplicitamente con sort / sort_by, oppure accetta il rumore posizionale.
Due trappole:
jqnon può modificare sul posto; scrive su standard output. Usa la process substitution (<(...)), oppure scrivi un file temporaneo, verifica, poi sostituisci;-rrimuove le virgolette dalle stringhe, quindi l'output può contenere newline o caratteri significativi per la shell. Quota i dati non fidati prima di passarli avanti con una pipe.
Cosa fa /diff di questo sito, e cosa non fa
Lo strumento di confronto JSON integrato segue lo stesso approccio strutturale: entrambi i lati vengono analizzati in alberi e allineati ricorsivamente per percorso, producendo percorso + tipo(add/remove/modify) + prima/dopo. L'ordine delle chiavi degli oggetti non influisce sul risultato, e tutto gira nel browser — i payload incollati non vengono mai caricati.
I suoi array, però, sono allineati per indice. È un compromesso deliberato, non un difetto:
- Aggiungere in coda, modificare la stessa posizione o sostituire in blocco ti dà un output pulito;
- Inserire un record in testa o nel mezzo segna tutto ciò che segue come
modify, e devi individuare che "in realtà solo un record è nuovo".
Quindi quando confronti liste di ordini, la mossa corretta è ordinare entrambi i lati per id prima di incollare. L'input ordinato fissa il punto di inserimento e riduce il rumore a una voce. Nessuna installazione necessaria, e copre la maggior parte dei casi quotidiani.
Se devi "generare una patch applicabile" o "rilevare elementi spostati", lo strumento in pagina non lo fa. È la sezione successiva.
Trasformare un diff in una patch: due formati, non mescolarli
Gli umani leggono i diff; i programmi applicano le patch. JSON ha due formati di patch con semantiche completamente diverse.
| Dimensione | JSON Patch (RFC 6902) | JSON Merge Patch (RFC 7396) |
|---|---|---|
| Forma | Array di operazioni con op, path, value |
Un oggetto JSON con la forma del documento di destinazione |
| Array | Sì, per indice, /items/0, /- per aggiungere |
No, gli array vengono sostituiti in blocco |
| Eliminare una chiave | remove |
Solo tramite null, che non può eliminare un campo il cui valore è legittimamente null |
| Esprimere uno spostamento | move / copy |
No |
| Si adatta a | Risorse che contengono array, log di audit, aggiornamenti atomici | Aggiornamenti parziali di oggetti semplici, consegna della configurazione |
JSON Patch ha un'operazione che le persone saltano: test.
[
{ "op": "test", "path": "/version", "value": 3 },
{ "op": "replace", "path": "/status", "value": "shipped" }
]
Se test fallisce, l'intera patch viene rifiutata. È concorrenza ottimistica integrata nel protocollo — nessun header If-Match extra necessario per garantire "ho scritto solo perché ho visto la versione 3". Per un endpoint PATCH vale più di qualsiasi convenzione documentale.
I percorsi usano JSON Pointer (RFC 6901): ~0 fa escape di ~, ~1 fa escape di /. Le chiavi che contengono slash (i tipi MIME come chiavi, per esempio) devono essere sottoposte a escape, altrimenti il pointer atterra altrove. I dettagli vivono in JSON Pointer e Patch.
Resta un problema pratico: le patch generate automaticamente da un diff di solito indirizzano gli array per indice. Gli indici sono fragili in produzione — un inserimento concorrente e /items/3 non punta più allo stesso record. O limita le patch generate ai campi oggetto, oppure valida i campi critici con test prima di applicare.
Mettere il confronto in CI: asserisci i valori, non il testo
Tre regole bastano:
- Asserisci i valori analizzati nei test, mai l'output di
JSON.stringify. In Jest usatoEqual; in Pythonassert a == bdopojson.loads. Ordine delle chiavi e indentazione non partecipano. - Metti in allowlist i campi dinamici.
requestId,timestamp,traceIddifferiscono a ogni esecuzione; senza esclusioni il test è permanentemente rosso.propertyFilterin jsondiffpatch eexclude_pathsin deepdiff esistono per questo. - Stampa i percorsi in caso di fallimento, non l'intero documento. Nessuno legge 5 MB di payload in un log;
$.data.items[id=ORD-042].qty: 2 → 3localizza il problema all'istante.
La deriva della configurazione funziona allo stesso modo: applica jq -S sia alle configurazioni di staging sia a quelle di produzione, ordina gli array per una chiave di business, e ciò che resta è una differenza reale. Questa classe di problemi — gli stessi dati che sembrano diversi tra ambienti — di solito è un contratto non definito, che è l'argomento di Progettazione dei contratti API.
FAQ
L'ordine delle chiavi conta come differenza?
No. RFC 8259 definisce gli oggetti come collezioni non ordinate, quindi {"a":1,"b":2} e {"b":2,"a":1} sono lo stesso valore. Qualsiasi strumento che segnala il riordino come modifica sta facendo confronto testuale, non confronto JSON.
Perché inserire un record segna ogni voce come modificata?
Perché lo strumento allinea gli array indice per indice. Il nuovo elemento sta alla posizione 0, ogni record esistente si sposta di uno, e nessuna coppia di posizioni corrisponde. Ordina o indicizza per una chiave come id prima di confrontare, invece di confrontare per posizione.
Gli strumenti di confronto online caricano i miei dati?
Dipende dall'implementazione. Gli strumenti renderizzati sul server inviano il payload a un backend, dove può essere registrato; gli strumenti client-side analizzano nel browser e non caricano nulla. Prima di confrontare configurazioni di produzione o payload utente, conferma che la pagina calcoli localmente, oppure disconnetti la rete e riprova.
Il risultato di un diff può essere usato direttamente come patch?
Non direttamente. Un diff dice dove le cose differiscono; JSON Patch dice quali operazioni eseguire in ordine. Tra loro c'è uno strato di conversione. Le patch generate tendono anche a indirizzare gli array per indice, il che si rompe sotto concorrenza — aggiungi operazioni test prima di applicare.
Come confronto file da decine di megabyte?
Usa prima lo streaming (jq -c scrive un record per riga), oppure estrai solo il sottoalbero che ti interessa, come jq '.data.items'. Un confronto ricorsivo completo di decine di megabyte sta ancora in memoria; oltre i cento megabyte, frammentalo o indicizzalo per chiave. Per cifre concrete su memoria e tempi, vedi prestazioni con JSON grande.
Devo ordinare gli array per id prima di confrontare?
Sì. Lo strumento di confronto JSON integrato allinea gli array per indice, quindi inserire un record in testa o nel mezzo segna tutto ciò che segue come modify; ordinare entrambi i lati per id prima di incollare fissa il punto di inserimento e riduce il rumore a una voce. L'eccezione è quando l'ordine dell'array è esso stesso un dato — passi di migrazione, classifiche di ricerca, punti di un grafico — dove il confronto posizionale è corretto e l'ordinamento è sbagliato.