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.

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:
- String kosong menunjuk node akar itu sendiri, bukan "node kosong";
- Setiap
/memisahkan satu token referensi; array menggunakan indeks desimal (nol di depan tidak diperbolehkan, jadi01tidak valid); - Di dalam token,
~ditulis~0dan/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:
- Target adalah objek dan patch adalah objek → gabungkan secara rekursif;
- Nilai key dalam patch adalah
null→ hapus key itu dari target; - Nilai key dalam patch bukan objek → ganti langsung;
- 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.nullsudah 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-Typeyang 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:
- Jangan mengubah objek bersama di tempat; patch yang gagal meninggalkan keadaan setengah termodifikasi (kecuali implementasi menjamin rollback);
- Validasi path: patch dari luar harus memvalidasi setiap
path/fromdan menolak key seperti__proto__danconstructor; - Batas ukuran: batasi jumlah entri patch untuk mencegah satu patch melakukan ratusan ribu operasi;
- 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-Typeyang 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
testatauIf-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.