Ketika dua dokumen JSON berbeda: diff posisional, diff berbasis id, atau normalisasi dulu
Intinya: Lima puluh record menjadi lima puluh satu dan diff melaporkan lima puluh satu perubahan. Membahas false positive text-diff, pencocokan array berdasarkan id, normalisasi jq, dan patch yang bisa diterapkan.

Intinya: normalisasi dulu, lalu bandingkan — gunakan jq -S atau serialisasi stabil untuk menghapus urutan key, indentasi, dan penulisan number, urutkan atau indeks array berdasarkan key bisnis ketika urutan tidak bermakna, dan baru setelah itu ubah diff menjadi patch.
Perbedaan JSON bukan perbedaan teks. Bandingkan file sebagai string dan penataan ulang key akan menyalakan seluruh halaman; bandingkan array berdasarkan indeks dan menyisipkan satu record akan menandai lima puluh lainnya sebagai berubah.
Sebuah patch dikirim ke layanan order, dan Anda hanya punya dua respons: lima puluh record sebelumnya, lima puluh satu setelahnya, dengan satu record baru disisipkan di tengah. Alat diff melaporkan lima puluh satu perubahan, dinding merah, dan Anda tetap tidak bisa tahu record mana yang sebenarnya bergerak.
Alatnya tidak rusak. "Membandingkan" berarti tiga hal berbeda pada JSON, dan kebanyakan orang secara default memilih yang paling tidak berguna.
Mereproduksi alarm palsu: satu sisipan, lima puluh satu perubahan
Tiga puluh baris Node 22: bangun lima puluh order ({id, sku, qty}), lalu tambahkan satu di ekor dan sisipkan satu di kepala, hitung perbedaan dengan penyelarasan indeks per indeks.
| Operasi | Record | Perubahan yang dilaporkan berdasarkan posisi |
|---|---|---|
| Tambah satu di ekor | 50 → 51 | 1 |
| Sisipkan satu di kepala | 50 → 51 | 51 |
"Kehadiran satu record" yang sama, lima puluh satu kali noise. Sebabnya biasa saja: ketika Anda membandingkan berdasarkan indeks, elemen baru mendarat di posisi 0 dan setiap record yang ada bergeser turun satu slot. Posisi 0 versus posisi 0, posisi 1 versus posisi 1 — tidak ada pasangan yang sama.
Bangun Map dengan key id sebagai gantinya dan data yang sama melaporkan tepat satu entri: $[id=ORD-999] added.
Eksperimen itu memberi Anda aturan yang lebih penting daripada rekomendasi alat apa pun: apakah array tersebut urutan terurut atau himpunan tak terurut yang menentukan perbandingan mana yang harus Anda gunakan.
Tiga cara text diff berbohong tentang JSON
diff a.json b.json membandingkan karakter. JSON membawa struktur data. Ketidaksesuaian itu menghasilkan tiga jenis false positive yang dapat diandalkan.
1. Key object yang tertata ulang
Bagian 4 RFC 8259 eksplisit: object adalah "koleksi tak terurut dari pasangan name/value". Dua dokumen ini adalah nilai yang sama:
{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }
Line diff menyebut ketiga baris berubah. Saya membalik urutan key object dengan sepuluh key, mencetak keduanya dengan pretty, dan membandingkan baris per baris: kesepuluh baris ditandai, sementara perbandingan struktural menyebut mereka sama. Rasio noise 10:0.
Mengapa urutan key berubah sama sekali? Berganti serializer, menjalankan formatter dengan key terurut, membangun ulang object dari dictionary atau hash map, berpindah antar runtime bahasa — salah satu saja cukup membuat data identik tampak berbeda di disk.
2. Whitespace dan indentasi
{"a":1} dan {\n "a": 1\n} menghasilkan nilai yang sama setelah di-parse. Text diff melihat newline dan spasi lalu menandai semuanya.
3. Penulisan number dan escape yang berbeda
1.0 dan 1, 1e2 dan 100, "\u00e9" dan "é" — nilai sama setelah parsing, teks berbeda setelah serialisasi. Yang ini lebih ringan karena menyentuh sedikit baris, tetapi dalam review konfigurasi cukup membuat Anda mengira sebuah number berubah.
Kasus sebaliknya adalah yang tidak bisa dilihat teks tetapi bisa dilihat struktur: 5 versus "5". Satu pasangan tanda kutip tambahan, tetapi tipenya melenceng — biasanya bug, bukan update. Perbandingan struktural melaporkannya sebagai modify tanpa ragu.
Tiga mode perbandingan, tiga pekerjaan
| Mode | Apa yang dibandingkan | Dari mana noise berasal | Output | Cocok untuk |
|---|---|---|---|---|
| Text diff | Baris karakter | Urutan key, indentasi, penulisan number | Nomor baris, merah/hijau | Bundle lokalisasi, fixture dengan urutan key stabil |
| Structural diff | Tree hasil parse, diselaraskan berdasarkan path | Array diselaraskan berdasarkan indeks | Path dengan add/remove/modify | Respons API, konfigurasi, webhook |
| Semantic diff | Tree plus key bisnis | Hanya pilihan key yang buruk | Path dengan deteksi move/insert | Daftar record, order, inventaris |
Structural diff menyelesaikan lapisan "urutan key dan format". Semantic diff melangkah lebih jauh dan menyelesaikan "identitas elemen array". Perbedaannya bukan kompleksitas implementasi; melainkan apakah Anda bersedia memberi tahu alat field mana yang menjadi paspor sebuah record.
Array adalah garis pemisah
Ajukan satu pertanyaan: apakah urutan array ini sendiri merupakan data?
- Urutan adalah data: langkah migrasi, peringkat pencarian, titik chart, rantai persetujuan. Perubahan posisi adalah perubahan perilaku, jadi perbandingan posisional benar.
- Urutan tidak bermakna: tag, himpunan ID izin, item baris order, daftar inventaris. Penyisipan seharusnya tidak mempengaruhi cara item lain dinilai.
Ketika urutan tidak bermakna, indeks record berdasarkan key alih-alih menghantam mereka berdasarkan posisi:
// Cocokkan berdasarkan id: satu sisipan atau hapus melaporkan satu entri, item lain tetap di tempatnya
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 mengubah ini menjadi opsi. jsondiffpatch memakai LCS untuk menemukan subsequence umum terpanjang, tetapi elemen object di dalam array dicocokkan berdasarkan posisi secara default — dokumentasi resminya menekankan bahwa Anda harus menyediakan fungsi objectHash untuk mencocokkannya berdasarkan isi. Pengguna pertama kali sering menyimpulkan pustaka itu bodoh; kenyataannya mereka tidak pernah memberikan key.
Memilih key juga penting. id adalah pilihan aman. sku hanya berfungsi ketika SKU unik. Melakukan hash seluruh object mengubah "satu field berubah" menjadi "satu dihapus plus satu ditambahkan", yang lebih buruk daripada membandingkan berdasarkan posisi.
Trik terminal tercepat: normalisasi dengan jq
Jika Anda tidak ingin pustaka dan hanya perlu melihat sekilas, normalisasi dulu dan diff kemudian. jq -S mengurutkan key object secara rekursif dan menerapkan indentasi yang konsisten:
# Normalisasi, lalu bandingkan baris per baris; noise urutan key dan indentasi hilang
diff -u <(jq -S . before.json) <(jq -S . after.json)
# Ketika array benar-benar tak terurut, urutkan berdasarkan key sebelum membandingkan
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} — key terurut, arr tidak disentuh. Ingat ini: -S hanya mengurutkan key object, tidak pernah elemen array. Urutkan array secara eksplisit dengan sort / sort_by, atau terima noise posisional.
Dua jebakan:
jqtidak bisa mengedit di tempat; ia menulis ke standard output. Gunakan process substitution (<(...)), atau tulis file sementara, verifikasi, lalu ganti;-rmenghapus tanda kutip dari string, sehingga output bisa berisi newline atau karakter signifikan shell. Kutip data yang tidak tepercaya sebelum meneruskannya.
Apa yang dilakukan /diff situs ini, dan apa yang tidak
Alat bandingkan JSON bawaan mengikuti pendekatan struktural yang sama: kedua sisi di-parse menjadi tree dan diselaraskan secara rekursif berdasarkan path, menghasilkan path + type(add/remove/modify) + before/after. Urutan key object tidak mempengaruhi hasil, dan semuanya berjalan di browser — payload yang ditempel tidak pernah diunggah.
Namun array-nya diselaraskan berdasarkan indeks. Itu trade-off yang disengaja, bukan cacat:
- Menambahkan di ekor, memodifikasi posisi yang sama, atau mengganti seluruhnya memberi Anda output bersih;
- Menyisipkan record di kepala atau di tengah menandai semua setelahnya sebagai
modify, dan Anda harus mengenali bahwa "sebenarnya hanya satu record yang baru".
Jadi ketika Anda membandingkan daftar order, langkah yang benar adalah mengurutkan kedua sisi berdasarkan id sebelum menempel. Input yang terurut memaku titik sisipan dan meruntuhkan noise menjadi satu entri. Tanpa instalasi, dan mencakup sebagian besar kasus sehari-hari.
Jika Anda butuh "menghasilkan patch yang bisa diterapkan" atau "mendeteksi elemen yang berpindah", alat dalam halaman tidak melakukan itu. Itu bagian berikutnya.
Mengubah diff menjadi patch: dua format, jangan dicampur
Manusia membaca diff; program menerapkan patch. JSON punya dua format patch dengan semantik yang sangat berbeda.
| Dimensi | JSON Patch (RFC 6902) | JSON Merge Patch (RFC 7396) |
|---|---|---|
| Bentuk | Array operasi dengan op, path, value |
Object JSON yang berbentuk seperti dokumen target |
| Array | Ya, berdasarkan indeks, /items/0, /- untuk menambahkan |
Tidak, array diganti utuh |
| Hapus key | remove |
Hanya via null, yang tidak bisa menghapus field yang nilainya memang null |
| Menyatakan pemindahan | move / copy |
Tidak |
| Cocok untuk | Resource berisi array, audit log, update atomik | Update parsial plain object, distribusi konfigurasi |
JSON Patch punya satu operasi yang sering dilewati orang: test.
[
{ "op": "test", "path": "/version", "value": 3 },
{ "op": "replace", "path": "/status", "value": "shipped" }
]
Jika test gagal, seluruh patch ditolak. Itu konkurensi optimistik yang tertanam di protokol — tidak perlu header If-Match tambahan untuk menjamin "saya hanya menulis karena saya melihat versi 3". Untuk endpoint PATCH ini lebih berharga daripada konvensi dokumentasi apa pun.
Path memakai JSON Pointer (RFC 6901): ~0 meng-escape ~, ~1 meng-escape /. Key yang berisi garis miring (misalnya MIME type sebagai key) harus di-escape, atau pointer mendarat di tempat lain. Detailnya ada di JSON Pointer dan Patch.
Satu masalah praktis tersisa: patch yang dihasilkan otomatis dari diff biasanya mengalamati array berdasarkan indeks. Indeks rapuh di produksi — satu sisipan bersamaan dan /items/3 tidak lagi menunjuk record yang sama. Entah batasi patch yang dihasilkan ke field object, atau validasi field kritis dengan test sebelum diterapkan.
Masukkan perbandingan ke CI: assert nilai, bukan teks
Tiga aturan sudah cukup:
- Assert nilai hasil parse di tes, jangan pernah output
JSON.stringify. Di Jest gunakantoEqual; di Pythonassert a == bsetelahjson.loads. Urutan key dan indentasi tidak berpartisipasi. - Whitelist field dinamis.
requestId,timestamp,traceIdberbeda setiap kali jalan; tanpa pengecualian, tes akan selalu merah.propertyFilterdi jsondiffpatch danexclude_pathsdi deepdiff ada untuk ini. - Cetak path saat gagal, bukan seluruh dokumen. Tidak ada yang membaca payload 5 MB di log;
$.data.items[id=ORD-042].qty: 2 → 3langsung menemukan masalahnya.
Config drift bekerja dengan cara yang sama: jq -S konfigurasi staging dan produksi, urutkan array berdasarkan key bisnis, dan apa pun yang tersisa adalah perbedaan nyata. Kelas masalah ini — data yang sama tampak berbeda antar lingkungan — biasanya kontrak yang tidak terdefinisi, yang menjadi subjek desain kontrak API.
FAQ
Apakah urutan key dihitung sebagai perbedaan?
Tidak. RFC 8259 mendefinisikan object sebagai koleksi tak terurut, jadi {"a":1,"b":2} dan {"b":2,"a":1} adalah nilai yang sama. Alat apa pun yang melaporkan penataan ulang sebagai perubahan sedang melakukan perbandingan teks, bukan perbandingan JSON.
Mengapa menyisipkan satu record menandai setiap entri sebagai berubah?
Karena alat menyelaraskan array indeks per indeks. Elemen baru duduk di posisi 0, setiap record yang ada bergeser turun satu, dan tidak ada dua posisi yang cocok. Urutkan atau indeks berdasarkan key seperti id sebelum membandingkan alih-alih membandingkan secara posisional.
Apakah alat perbandingan online mengunggah data saya?
Tergantung implementasinya. Alat yang dirender server mengirim payload ke backend, di mana ia mungkin dicatat; alat sisi klien mem-parsing di browser dan tidak mengunggah apa pun. Sebelum membandingkan konfigurasi produksi atau payload pengguna, pastikan halaman menghitung secara lokal, atau putuskan jaringan dan coba lagi.
Bisakah hasil diff langsung dipakai sebagai patch?
Tidak langsung. Diff menyatakan di mana hal berbeda; JSON Patch menyatakan operasi mana yang dijalankan dalam urutan apa. Lapisan konversi berada di antara keduanya. Patch yang dihasilkan juga cenderung mengalamati array berdasarkan indeks, yang rusak di bawah konkurensi — tambahkan operasi test sebelum menerapkan.
Bagaimana cara membandingkan file berukuran puluhan megabyte?
Stream dulu (jq -c menulis satu record per baris), atau ekstrak hanya subtree yang Anda pedulikan, misalnya jq '.data.items'. Perbandingan rekursif penuh atas puluhan megabyte masih muat di memori; di atas seratus megabyte, pecah atau indeks berdasarkan key. Untuk angka memori dan waktu konkret, lihat performa JSON besar.
Haruskah saya mengurutkan array berdasarkan id sebelum membandingkan?
Ya. Alat bandingkan JSON bawaan menyelaraskan array berdasarkan indeks, jadi menyisipkan satu record di kepala atau di tengah menandai semua setelahnya sebagai modify; mengurutkan kedua sisi berdasarkan id sebelum menempel memaku titik sisipan dan meruntuhkan noise menjadi satu entri. Pengecualiannya adalah ketika urutan array itu sendiri adalah data — langkah migrasi, peringkat pencarian, titik chart — di mana perbandingan posisional benar dan pengurutan salah.