JSON Toolkit

JSON Pointer, Patch, dan Merge Patch

Intinya: Tiga spesifikasi singkat membentuk protokol perubahan: Pointer melokasikan, Patch mengedit secara tepat, Merge Patch menggabungkan sebagian. Salah memilih berdampak pada array dan konkurensi.

Menengah±12 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul untuk JSON Pointer, Patch, dan Merge Patch

Intinya: gunakan JSON Pointer untuk melokasikan berdasarkan posisi dan JSONPath untuk memfilter atau rekursi; serahkan penyisipan/penghapusan array, penulisan null, dan pernyataan versi kepada JSON Patch, dan sisakan Merge Patch untuk resource konfigurasi datar dan form.

Melokasikan dan memodifikasi adalah dua hal berbeda, dan spesifikasi memisahkannya menjadi tiga dokumen. Harga umum dari mencampuradukkannya adalah "seluruh array tergantikan" dan tidak ada yang menyadarinya.

1. JSON Pointer: Sintaks Pelokasian Terpendek

RFC 6901 melakukan tepat satu hal: menunjuk node dalam dokumen dengan sebuah string.

""             → the whole document
"/store"       → the store member under the root
"/store/book"  → one level deeper
"/store/book/0/title" → item 0 of the array

Tiga aturan:

  1. String kosong menunjuk node akar itu sendiri, bukan "node kosong";
  2. Setiap / memisahkan satu token referensi; array menggunakan indeks desimal (nol di depan tidak diperbolehkan, jadi 01 tidak valid);
  3. Di dalam token, ~ ditulis ~0 dan / sebagai ~1 — ganti ~ sebelum /, jika tidak Anda berakhir dengan double escaping.
/**
 * Meng-escape satu token referensi menjadi segmen JSON Pointer.
 * @param token Nama key atau indeks asli
 * @returns Token yang telah di-escape
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * Menggabungkan array nama key menjadi JSON Pointer.
 * @param tokens Array nama key atau indeks
 * @returns String pointer yang diawali dengan /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

Nilainya paling terlihat ketika key itu sendiri mengandung /: pointer untuk {"a/b": 1} adalah /a~1b, bukan /a/b (yang terakhir berarti b di bawah a).

Ketika digunakan di dalam URL, percent-encoding juga diperlukan, karena # dan ~ memiliki makna khusus dalam fragmen.

2. JSON Patch: Urutan Atomik dari Enam Operasi

RFC 6902 mendeskripsikan perubahan dengan array operasi, media type application/json-patch+json:

[
  { "op": "test", "path": "/version", "value": 7 },
  { "op": "replace", "path": "/name", "value": "新的名字" },
  { "op": "add", "path": "/tags/-", "value": "json" },
  { "op": "remove", "path": "/temp" },
  { "op": "copy", "from": "/name", "path": "/alias" },
  { "op": "move", "from": "/tags/0", "path": "/tags/1" }
]
Operasi Semantik Batas
add Menambahkan anggota atau menyisipkan elemen array Indeks array boleh sama dengan panjang (append); - berarti akhir
remove Menghapus Path harus ada
replace Mengganti nilai yang sudah ada Path harus ada (setara remove + add, tetapi mengharuskan path ada lebih dulu)
move Memindahkan Anda tidak dapat memindahkan node ke subpath-nya sendiri
copy Menyalin Sumber harus ada
test Menyatakan kesamaan Jika gagal, seluruh patch tidak diterapkan; digunakan untuk penguncian optimistis

Properti kuncinya adalah atomisitas: jika ada operasi yang gagal, seluruh patch di-rollback, sehingga tidak ada keadaan perantara di mana "tiga pertama diterapkan dan yang keempat gagal". Ini menjadikannya body request PATCH yang bagus.

2.1 Menggunakan test untuk konkurensi optimistis

[
  { "op": "test", "path": "/etag", "value": "a3f9c1" },
  { "op": "replace", "path": "/status", "value": "paid" }
]

Klien terlebih dahulu melakukan GET pada etag dan menuliskannya ke operasi test. Pada penulisan konkuren, test dari request kedua gagal dan server mengembalikan 409 Conflict atau 412 Precondition Failed. Ini jauh lebih aman daripada last-write-wins dan tidak memerlukan kunci terdistribusi.

2.2 Operasi array paling rawan kesalahan

// Want to insert at index 1
{ "op": "add", "path": "/items/1", "value": {...} }   // Correct: insert; the original index 1 and everything after shift back

// Want to replace index 1
{ "op": "replace", "path": "/items/1", "value": {...} } // Correct: replace

add pada indeks array n adalah penyisipan, bukan penimpaan; indeks di luar jangkauan (n > length) harus menimbulkan error. Beberapa implementasi longgar dalam hal ini, yang membuat patch yang sama berperilaku berbeda antar server.

3. Merge Patch: Fragmen yang Mirip Target

RFC 7386 hanya memiliki empat aturan:

  1. Target adalah objek dan patch adalah objek → gabungkan secara rekursif;
  2. Nilai key dalam patch adalah null → hapus key itu dari target;
  3. Nilai key dalam patch bukan objek → ganti langsung;
  4. Patch itu sendiri bukan objek → seluruh target diganti.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

// Patch
{ "a": "z", "c": { "f": null } }

// Result
{ "a": "z", "c": { "d": "e" } }

Media type-nya adalah application/merge-patch+json. Ia terbaca seperti "update parsial", yang membuatnya sangat mudah disalahgunakan:

  • Array hanya dapat diganti seluruhnya. Ingin menghapus list[2]? Merge Patch tidak bisa melakukannya;
  • Anda tidak dapat menulis null. null sudah dipakai untuk berarti penghapusan;
  • Ia tidak dapat mengekspresikan keadaan ketiga antara "biarkan field ini" dan "hapus field ini" (misalnya, secara eksplisit mengaturnya menjadi kosong).

4. Matriks Kemampuan: Mana yang Dipilih

Kebutuhan JSON Patch Merge Patch
Mengganti elemen array tertentu secara tepat ✓ ✗
Menghapus satu item dari array ✓ ✗
Memperbarui beberapa field secara parsial ✓ (beberapa replace) ✓ (paling ringkas)
Menulis nilai null ✓ ✗
Kontrol konkurensi (pernyataan versi) ✓ (test) ✗ (hanya melalui If-Match)
Keterbacaan patch Sedang Baik (terlihat seperti data itu sendiri)
Kompleksitas implementasi Tinggi Rendah
Bentuk resource yang cocok Bersarang dalam, mirip list Konfigurasi datar, form

Aturan praktis:

  • Form, halaman pengaturan, resource konfigurasi → Merge Patch; frontend hanya mengirim field yang berubah;
  • Penambahan dan penghapusan list, transisi state machine, apa pun yang membutuhkan perlindungan konkurensi → JSON Patch;
  • Keduanya dapat hidup berdampingan: endpoint berbeda mendeklarasikan Content-Type yang berbeda, dan server mengirim berdasarkan tipe.

5. Catatan Implementasi

import { applyPatch, compare } from 'fast-json-patch';

const document = { version: 7, items: [{ id: 1 }, { id: 2 }] };
const patch = compare(document, { version: 8, items: [{ id: 1 }] });

// test operations are validated by default; setting mutateDocument to true modifies in place, use with caution
const result = applyPatch(document, patch, true, false);
console.log(result.newDocument);

Empat poin yang mudah terlewat:

  1. Jangan mengubah objek bersama di tempat; patch yang gagal meninggalkan keadaan setengah termodifikasi (kecuali implementasi menjamin rollback);
  2. Validasi path: patch dari luar harus memvalidasi setiap path/from dan menolak key seperti __proto__ dan constructor;
  3. Batas ukuran: batasi jumlah entri patch untuk mencegah satu patch melakukan ratusan ribu operasi;
  4. Auditing: patch secara alami cocok untuk audit log — mereka adalah deskripsi lengkap "apa yang berubah".

6. Semantik Error

Situasi Kode status yang disarankan Penjelasan
Patch bukan array / tidak valid secara struktur 400 Bad Request Format body request salah
Node yang dirujuk path tidak ada 422 Unprocessable Entity Valid secara sintaks tetapi tidak dapat dieksekusi secara semantik
Operasi test gagal 409 atau 412 Menandakan konflik versi
Tidak ada izin untuk mengubah path tersebut 403 Forbidden Jangan menutupinya dengan 404 kecuali Anda sengaja menyembunyikan keberadaan resource

Membedakan 400 dari 422 penting: yang pertama berarti "saya tidak dapat memahami patch Anda", yang terakhir berarti "saya memahaminya, tetapi data ini tidak mengizinkan perubahan itu".

7. Daftar Periksa

  • [ ] Endpoint mendeklarasikan Content-Type yang benar (json-patch+json / merge-patch+json);
  • [ ] Penerapan patch bersifat atomik, dan kegagalan tidak meninggalkan hasil parsial;
  • [ ] Patch eksternal divalidasi terhadap whitelist path dan batas jumlah entri;
  • [ ] Kontrol konkurensi menggunakan test atau If-Match;
  • [ ] Patch ditulis ke audit log;
  • [ ] Skenario array secara eksplisit memilih JSON Patch; jangan mengandalkan Merge Patch.

FAQ

Bagaimana saya memutuskan antara JSON Patch dan Merge Patch?

Gunakan JSON Patch ketika Anda perlu mengganti atau menghapus elemen array tertentu, menulis nilai null, atau menyatakan versi; gunakan Merge Patch untuk form dan resource konfigurasi yang hanya memperbarui beberapa field. Merge Patch tidak dapat menghapus satu item array, dan tidak dapat menulis null, karena null sudah berarti penghapusan.

Mengapa add pada indeks array n bukan penimpaan?

Karena add menyisipkan ke dalam array: elemen pada indeks n dan semua setelahnya bergeser mundur. Untuk mengganti elemen yang sudah ada Anda harus menggunakan replace, dan path harus ada terlebih dahulu. Indeks di luar jangkauan (n > length) harus menimbulkan error; implementasi yang longgar membuat patch yang sama berperilaku berbeda antar server.

Haruskah saya mengubah objek asli di tempat?

Tidak. Patch yang gagal meninggalkan keadaan setengah termodifikasi kecuali implementasi menjamin rollback. Terapkan patch pada salinan hasil deep copy atau structured clone, lalu tukar referensi setelah berhasil.