JSON Toolkit

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.

Principiante~6 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Copertina del confronto di due documenti JSON

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:

  • jq non può modificare sul posto; scrive su standard output. Usa la process substitution (<(...)), oppure scrivi un file temporaneo, verifica, poi sostituisci;
  • -r rimuove 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:

  1. Asserisci i valori analizzati nei test, mai l'output di JSON.stringify. In Jest usa toEqual; in Python assert a == b dopo json.loads. Ordine delle chiavi e indentazione non partecipano.
  2. Metti in allowlist i campi dinamici. requestId, timestamp, traceId differiscono a ogni esecuzione; senza esclusioni il test è permanentemente rosso. propertyFilter in jsondiffpatch e exclude_paths in deepdiff esistono per questo.
  3. 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 → 3 localizza 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.