JSON Toolkit

Apabila dua dokumen JSON berbeza: diff kedudukan, diff berasaskan id, atau normalisasi dahulu

Inti pati: Lima puluh rekod menjadi lima puluh satu dan diff melaporkan lima puluh satu perubahan. Merangkumi positif palsu diff teks, memadankan tatasusunan mengikut id, normalisasi jq dan patch yang boleh digunakan.

Pemula~6 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit untuk membandingkan dua dokumen JSON

Kesimpulan utama: normalkan dahulu, kemudian bandingkan — gunakan jq -S atau pensirian stabil untuk menghapuskan susunan kunci, inden, dan ejaan nombor, isih atau indeks tatasusunan mengikut kunci perniagaan apabila susunan tidak bermakna, dan hanya selepas itu tukar diff menjadi patch.

Perbezaan JSON bukan perbezaan teks. Bandingkan fail sebagai string dan penyusunan semula kunci menyala seluruh halaman; bandingkan tatasusunan mengikut indeks dan menyisipkan satu rekod menandakan lima puluh yang lain sebagai berubah.

Satu patch dihantar ke perkhidmatan pesanan, dan yang anda ada hanyalah dua respons: lima puluh rekod sebelum, lima puluh satu selepas, dengan satu rekod baharu disisipkan di tengah. Alat diff melaporkan lima puluh satu perubahan, dinding merah, dan anda masih tidak dapat membezakan rekod mana yang benar-benar bergerak.

Alat itu tidak rosak. "Membandingkan" bermaksud tiga perkara berbeza pada JSON, dan kebanyakan orang secara lalai mencapai yang paling kurang berguna.

Menghasilkan semula amaran palsu: satu sisipan, lima puluh satu perubahan

Tiga puluh baris Node 22: bina lima puluh pesanan ({id, sku, qty}), kemudian tambah satu di hujung dan sisipkan satu di hadapan, mengira perbezaan dengan penjajaran indeks demi indeks.

Operasi Rekod Perubahan dilaporkan mengikut kedudukan
Tambah satu di hujung 50 → 51 1
Sisipkan satu di hadapan 50 → 51 51

Sama "satu rekod tambahan", lima puluh satu kali bunyi. Alasannya biasa: apabila anda membandingkan mengikut indeks, elemen baharu mendarat di kedudukan 0 dan setiap rekod sedia ada beralih turun satu slot. Kedudukan 0 berbanding kedudukan 0, kedudukan 1 berbanding kedudukan 1 — tiada pasangan yang sama.

Bina Map berkunci id sebaliknya dan data yang sama melaporkan tepat satu entri: $[id=ORD-999] added.

Eksperimen itu memberikan anda peraturan yang lebih penting daripada sebarang syor alat: sama ada tatasusunan ialah jujukan tersusun atau set tidak tersusun menentukan perbandingan mana yang harus anda gunakan.

Tiga cara diff teks berbohong tentang JSON

diff a.json b.json membandingkan aksara. JSON membawa struktur data. Ketidakpadanan itu menghasilkan tiga jenis positif palsu yang boleh dijangka.

1. Kunci objek disusun semula

Bahagian 4 RFC 8259 eksplisit: objek ialah "koleksi pasangan nama/nilai yang tidak tersusun". Dua dokumen ini ialah nilai yang sama:

{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }

Diff baris memanggil ketiga-tiga baris berubah. Saya membalikkan susunan kunci objek sepuluh kunci, mencetak cantik kedua-duanya, dan membandingkan baris demi baris: kesemua sepuluh baris ditandakan, manakala perbandingan struktur mengatakan ia sama. Nisbah bunyi 10:0.

Mengapa susunan kunci berubah sama sekali? Bertukar pensiri, menjalankan pemformat dengan kunci tersusun, membina semula objek daripada kamus atau peta hash, berpindah antara masa jalan bahasa — mana-mana satu sudah cukup untuk membuat data yang sama kelihatan berbeza di cakera.

2. Ruang putih dan inden

{"a":1} dan {\n "a": 1\n} dihurai kepada nilai yang sama. Diff teks melihat baris baharu dan ruang dan menandakan segala-galanya.

3. Ejaan nombor dan escape yang berbeza

1.0 dan 1, 1e2 dan 100, "\u00e9" dan "é" — nilai sama selepas penghuraian, teks berbeza selepas pensirian. Yang ini lebih ringan kerana ia menyentuh sedikit baris, tetapi dalam semakan konfigurasi ia sudah cukup membuat anda fikir nombor berubah.

Kes terbalik ialah apa yang teks tidak dapat lihat dan struktur boleh: 5 berbanding "5". Satu pasangan petikan tambahan, tetapi jenisnya hanyut — biasanya bug dan bukannya kemas kini. Perbandingan struktur melaporkannya sebagai modify tanpa teragak-agak.

Tiga mod perbandingan, tiga tugas

Mod Apa yang dibandingkan Bunyi datang daripada Output Sesuai
Diff teks Baris aksara Susunan kunci, inden, ejaan nombor Nombor baris, merah/hijau Bundel penyetempatan, fixture dengan susunan kunci stabil
Diff struktur Pokok dihurai, dijajarkan mengikut laluan Tatasusunan dijajarkan mengikut indeks Laluan dengan add/remove/modify Respons API, konfigurasi, webhook
Diff semantik Pokok serta kunci perniagaan Hanya pilihan kunci yang buruk Laluan dengan pengesanan move/insert Senarai rekod, pesanan, inventori

Diff struktur menyelesaikan lapisan "susunan kunci dan pemformatan". Diff semantik melangkah satu lagi dan menyelesaikan "identiti elemen tatasusunan". Perbezaannya bukan kerumitan pelaksanaan; ia adalah sama ada anda sanggup memberitahu alat medan mana yang menjadi pasport rekod.

Tatasusunan ialah garis pemisah

Tanya satu soalan: adakah susunan tatasusunan ini sendiri data?

  • Susunan ialah data: langkah migrasi, kedudukan carian, titik carta, rantaian kelulusan. Perubahan kedudukan ialah perubahan tingkah laku, jadi perbandingan kedudukan adalah betul.
  • Susunan tidak bermakna: tag, set ID kebenaran, item baris pesanan, senarai inventori. Sisipan tidak sepatutnya menjejaskan bagaimana item lain dinilai.

Apabila susunan tidak bermakna, indeks rekod mengikut kunci dan bukannya memukulnya mengikut kedudukan:

// 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;
}

Pustaka matang menukar ini menjadi pilihan. jsondiffpatch menggunakan LCS untuk mencari subjujukan sepunya terpanjang, tetapi elemen objek di dalam tatasusunan dipadankan mengikut kedudukan secara lalai — dokumen rasmi menekankan bahawa anda mesti membekalkan fungsi objectHash untuk memadankannya mengikut kandungan. Pengguna kali pertama sering menyimpulkan pustaka itu bodoh; pada hakikatnya mereka tidak pernah memberikan kunci.

Memilih kunci juga penting. id ialah pilihan selamat. sku berfungsi hanya apabila SKU unik. Mencincang keseluruhan objek menukar "satu medan berubah" menjadi "satu dibuang ditambah satu ditambah", yang lebih buruk daripada membandingkan mengikut kedudukan.

Trik terminal terpantas: normalkan dengan jq

Jika anda tidak mahu pustaka dan hanya perlu melihat, normalkan dahulu dan diff kedua. jq -S mengisih kunci objek secara rekursif dan menggunakan inden yang konsisten:

# 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)

Saya menjalankan printf '{"b":2,"a":1,"arr":[1,2]}' | jq -S . dan mendapat {"a":1,"arr":[1,2],"b":2} — kunci diisih, arr tidak disentuh. Ingat ini: -S mengisih kunci objek sahaja, tidak pernah elemen tatasusunan. Sama ada isih tatasusunan secara eksplisit dengan sort / sort_by, atau terima bunyi kedudukan.

Dua perangkap:

  • jq tidak boleh menyunting di tempatnya; ia menulis ke output standard. Gunakan penggantian proses (<(...)), atau tulis fail sementara, sahkan, kemudian gantikan;
  • -r membuang petikan daripada string, jadi output mungkin mengandungi baris baharu atau aksara penting shell. Petik data tidak dipercayai sebelum menyalurkannya ke hadapan.

Apa yang /diff laman ini lakukan, dan apa yang tidak

Alat banding JSON terbina dalam mengikut pendekatan struktur yang sama: kedua-dua belah dihurai menjadi pokok dan dijajarkan secara rekursif mengikut laluan, menghasilkan path + type(add/remove/modify) + before/after. Susunan kunci objek tidak menjejaskan hasil, dan semuanya berjalan dalam pelayar — muatan yang ditampal tidak pernah dimuat naik.

Bagaimanapun tatasusunannya dijajarkan mengikut indeks. Itu pertukaran yang disengajakan, bukan kecacatan:

  • Menambah di hujung, mengubah kedudukan yang sama, atau mengganti sepenuhnya memberikan anda output bersih;
  • Menyisipkan rekod di hadapan atau di tengah menandakan segala-galanya selepasnya sebagai modify, dan anda perlu mengesan bahawa "sebenarnya hanya satu rekod baharu".

Jadi apabila anda membandingkan senarai pesanan, langkah yang betul ialah isih kedua-dua belah mengikut id sebelum menampal. Input yang diisih menetapkan titik sisipan dan meruntuhkan bunyi kepada satu entri. Tiada pemasangan diperlukan, dan ia meliputi kebanyakan kes harian.

Jika anda memerlukan "jana patch yang boleh digunakan" atau "kesan elemen yang berpindah", alat dalam halaman tidak melakukan itu. Itulah bahagian seterusnya.

Manusia membaca diff; program menggunakan patch. JSON mempunyai dua format patch dengan semantik yang sama sekali berbeza.

Dimensi JSON Patch (RFC 6902) JSON Merge Patch (RFC 7396)
Bentuk Tatasusunan operasi dengan op, path, value Objek JSON berbentuk seperti dokumen sasaran
Tatasusunan Ya, mengikut indeks, /items/0, /- untuk menambah Tidak, tatasusunan diganti sepenuhnya
Padam kunci remove Hanya melalui null, yang tidak boleh memadam medan yang nilainya sah null
Nyatakan pemindahan move / copy Tidak
Sesuai Sumber yang mengandungi tatasusunan, log audit, kemas kini atom Kemas kini separa objek biasa, penghantaran konfigurasi

JSON Patch mempunyai satu operasi yang orang langkau: test.

[
  { "op": "test", "path": "/version", "value": 3 },
  { "op": "replace", "path": "/status", "value": "shipped" }
]

Jika test gagal, keseluruhan patch ditolak. Itu konkurensi optimistik terbina dalam protokol — tiada pengepala If-Match tambahan diperlukan untuk menjamin "saya hanya menulis kerana saya melihat versi 3". Untuk endpoint PATCH ini lebih bernilai daripada mana-mana konvensyen dokumentasi.

Laluan menggunakan JSON Pointer (RFC 6901): ~0 mengescape ~, ~1 mengescape /. Kunci yang mengandungi garis miring (jenis MIME sebagai kunci, contohnya) mesti diescape, atau penunjuk mendarat di tempat lain. Butirannya ada dalam JSON Pointer dan Patch.

Satu masalah praktikal kekal: patch yang dijana secara automatik daripada diff biasanya menangani tatasusunan mengikut indeks. Indeks rapuh dalam pengeluaran — satu sisipan serentak dan /items/3 tidak lagi menunjuk rekod yang sama. Sama ada hadkan patch yang dijana kepada medan objek, atau sahkan medan kritikal dengan test sebelum menggunakan.

Letakkan perbandingan dalam CI: tuntut nilai, bukan teks

Tiga peraturan sudah cukup:

  1. Tuntut nilai yang dihurai dalam ujian, jangan sekali-kali output JSON.stringify. Dalam Jest gunakan toEqual; dalam Python assert a == b selepas json.loads. Susunan kunci dan inden tidak mengambil bahagian.
  2. Senarai putih medan dinamik. requestId, timestamp, traceId berbeza setiap larian; tanpa pengecualian ujian kekal merah. propertyFilter dalam jsondiffpatch dan exclude_paths dalam deepdiff wujud untuk ini.
  3. Cetak laluan apabila gagal, bukan keseluruhan dokumen. Tiada siapa membaca 5 MB muatan dalam log; $.data.items[id=ORD-042].qty: 2 → 3 mencari masalah serta-merta.

Hanyutan konfigurasi berfungsi dengan cara yang sama: jq -S kedua-dua konfigurasi staging dan pengeluaran, isih tatasusunan mengikut kunci perniagaan, dan apa yang tinggal ialah perbezaan sebenar. Kelas masalah ini — data yang sama kelihatan berbeza merentas persekitaran — biasanya kontrak yang tidak ditakrifkan, iaitu apa yang dibincangkan reka bentuk kontrak API.

FAQ

Adakah susunan kunci dikira sebagai perbezaan?

Tidak. RFC 8259 mentakrifkan objek sebagai koleksi tidak tersusun, jadi {"a":1,"b":2} dan {"b":2,"a":1} ialah nilai yang sama. Mana-mana alat yang melaporkan penyusunan semula sebagai perubahan sedang melakukan perbandingan teks, bukan perbandingan JSON.

Mengapa menyisipkan satu rekod menandakan setiap entri sebagai berubah?

Kerana alat menjajarkan tatasusunan indeks demi indeks. Elemen baharu berada di kedudukan 0, setiap rekod sedia ada beralih turun satu, dan tiada dua kedudukan sepadan. Isih atau indeks mengikut kunci seperti id sebelum membandingkan dan bukannya membandingkan mengikut kedudukan.

Adakah alat perbandingan dalam talian memuat naik data saya?

Ia bergantung pada pelaksanaan. Alat yang dipaparkan pelayan menghantar muatan ke backend, di mana ia mungkin dicatat; alat sisi klien menghurai dalam pelayar dan tidak memuat naik apa-apa. Sebelum membandingkan konfigurasi pengeluaran atau muatan pengguna, pastikan halaman mengira secara setempat, atau putuskan sambungan rangkaian dan cuba semula.

Bolehkah hasil diff digunakan terus sebagai patch?

Tidak secara langsung. Diff mengatakan di mana perkara berbeza; JSON Patch mengatakan operasi mana yang perlu dijalankan mengikut urutan. Lapisan penukaran berada di antara mereka. Patch yang dijana juga cenderung menangani tatasusunan mengikut indeks, yang pecah di bawah konkurensi — tambah operasi test sebelum menggunakan.

Bagaimana saya membandingkan fail berpuluh megabait?

Strim dahulu (jq -c menulis satu rekod per baris), atau ekstrak hanya subtree yang anda pedulikan, seperti jq '.data.items'. Perbandingan rekursif penuh berpuluh megabait masih muat dalam memori; melebihi seratus megabait, serpihkan atau indeks mengikut kunci. Untuk angka memori dan masa yang konkrit, lihat prestasi JSON besar.

Patutkah saya mengisih tatasusunan mengikut id sebelum membandingkan?

Ya. Alat banding JSON terbina dalam menjajarkan tatasusunan mengikut indeks, jadi menyisipkan satu rekod di hadapan atau di tengah menandakan segala-galanya selepasnya sebagai modify; mengisih kedua-dua belah mengikut id sebelum menampal menetapkan titik sisipan dan meruntuhkan bunyi kepada satu entri. Pengecualian ialah apabila susunan tatasusunan itu sendiri data — langkah migrasi, kedudukan carian, titik carta — di mana perbandingan kedudukan adalah betul dan pengisihan adalah salah.