JSON Toolkit

JSON Patch di Produksi: Atomisitas, Idempotensi, dan Konflik Konkuren

Intinya: JSON Patch hanyalah format perubahan; atomisitas, idempotensi, dan konkurensi tetap tanggung jawab Anda. Mencakup prasyarat test, escaping path, rollback, dan koordinasi ETag.

Lanjutan±10 menit bacaDiterbitkan 2026-10-31Diperbarui 2026-10-31
Sampul untuk JSON Patch di Produksi

Intinya: validasi dulu path target dan izin patch, lalu tambahkan pernyataan konkurensi dengan test atau If-Match; terapkan patch pada salinan agar kegagalan tidak meninggalkan apa pun setengah tertulis.

Bahaya nyata dari request PATCH bukanlah menulis field yang salah. Bahayanya adalah meyakini penulisan berhasil sementara diam-diam menimpa perubahan orang lain. JSON Patch memberi Anda alatnya, tetapi desain keamanan tetap milik Anda.

1. Tinjauan Singkat Himpunan Operasi RFC 6902

JSON Patch mendeskripsikan perubahan sebagai array operasi, dengan media type application/json-patch+json. Enam operasi dijalankan sesuai urutan array:

Operasi Efek Batasan kunci
add Menambahkan anggota atau menyisipkan elemen array Indeks array boleh sama dengan panjang; - berarti akhir
remove Menghapus anggota atau elemen Path target harus ada
replace Mengganti nilai yang sudah ada Path target harus ada
move Memindahkan node Tidak boleh memindahkan node ke path anaknya sendiri
copy Menyalin node Path sumber harus ada
test Menyatakan kesamaan nilai Jika gagal, seluruh patch tidak diterapkan

Banyak orang hanya mengingat tiga operasi pertama dan lupa bahwa test adalah yang mengubah penulisan buta menjadi penulisan bersyarat. Semantik operasi lengkap dan batas array dibahas di JSON Pointer, Patch, dan Merge Patch.

2. Menggunakan test untuk Mengubah Patch menjadi Update Bersyarat

Patch penguncian optimistis yang khas tidak dimulai dengan penulisan, melainkan dengan pernyataan versi:

{"op":"test","path":"/version","value":1}

Letakkan ke dalam patch lengkap:

[
  { "op": "test", "path": "/version", "value": 1 },
  { "op": "replace", "path": "/version", "value": 2 },
  { "op": "replace", "path": "/status", "value": "paid" }
]

Logikanya: konfirmasi bahwa versi dokumen saat ini adalah 1, lalu ubah menjadi 2. Jika request lain telah mendahulukan versi ke 2, test pertama gagal, seluruh patch tidak diterapkan, dan server mengembalikan konflik. Ini mencegah update yang hilang tanpa memperkenalkan kunci terdistribusi.

Beberapa poin desain:

  • Letakkan test sebelum setiap penulisan. Jika penulisan sebelumnya sudah berlaku, pernyataannya tidak bermakna;
  • Nyatakan pada nomor versi atau digest yang dihasilkan server, bukan pada field biasa yang bisa diubah bebas oleh klien;
  • Naikkan versi pada setiap penulisan yang berhasil; jika tidak, dua request yang menggunakan versi lama yang sama akan lolos keduanya;
  • value dari test harus memiliki tipe JSON yang persis sama dengan nilai dokumen. 1 dan "1" tidak sama.

3. Escaping Path dan Pencemaran Prototipe

Aturan escaping JSON Pointer sederhana: tulis ~ sebagai ~0 dan / sebagai ~1, dan ganti ~ sebelum /, jika tidak Anda mendapatkan double escaping.

Key "a/b"    → /a~1b
Key "m~n"    → /m~0n
Key "a~/b"   → /a~0~1b

Risiko pada patch eksternal biasanya bukan escaping melainkan key berbahaya. Di JavaScript, key seperti __proto__, constructor, dan prototype dapat memicu pencemaran prototipe, bahkan melalui add biasa:

[
  { "op": "add", "path": "/__proto__/polluted", "value": true }
]

Gunakan allowlist, bukan denylist:

  1. Izinkan patch hanya mengubah field bisnis, seperti /status atau /items/0/quantity;
  2. Validasi setiap path dan from terhadap ekspresi reguler atau awalan path, tolak level tak terduga selain indeks array;
  3. Uraikan JSON ke dalam wadah Object.create(null) atau Map untuk mengurangi dampak rantai prototipe yang tercemar;
  4. Batasi jumlah operasi dan ukuran byte setiap value.

4. Penerapan Atomik dan Rollback

RFC 6902 mensyaratkan eksekusi atomik, tetapi atomisitas adalah persyaratan semantik, bukan bawaan di setiap library. Library seperti fast-json-patch mengubah dokumen di tempat ketika mutateDocument bernilai true, dan kegagalan dapat meninggalkan perubahan sebagian.

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

/**
 * Menerapkan patch secara immutable dan mengembalikan dokumen asli saat gagal.
 * @param document Dokumen asli, yang tidak boleh diubah oleh pemanggil
 * @param patch Array operasi JSON Patch yang akan diterapkan
 * @returns Hasil, termasuk flag keberhasilan dan dokumen baru
 */
function applySafely(document: unknown, patch: unknown[]) {
  const errors = validate(patch, document);

  if (errors) {
    return { ok: false, document };
  }

  try {
    const result = applyPatch(document, patch, false, false);
    return { ok: true, document: result.newDocument };
  } catch {
    return { ok: false, document };
  }
}

Tiga rekomendasi implementasi:

  • Salin dalam atau structured-clone dulu, terapkan pada salinan, dan tukar referensi hanya setelah berhasil;
  • Jangan mengandalkan rollback otomatis library; perilaku pada exception berbeda antar implementasi;
  • Validasi sebelum eksekusi: validate hanya memeriksa struktur, sementara konflik semantik tetap memerlukan test; keduanya tidak saling menggantikan.

Jika patch mencakup beberapa resource, seperti mengubah pesanan dan inventarisnya sekaligus, atomisitas JSON Patch hanya berlaku dalam satu dokumen. Lintas dokumen Anda memerlukan transaksi basis data atau tindakan kompensasi.

5. Idempotensi: PATCH Tidak Secara Alami Idempoten

Semantik HTTP mendefinisikan PUT dan DELETE sebagai idempoten, tetapi PATCH belum tentu demikian. Mengulang JSON Patch yang sama dapat menghasilkan hasil yang benar-benar berbeda:

  • add /items/- menambahkan ke akhir array setiap kali, sehingga percobaan ulang menciptakan duplikat;
  • replace /version mengubah versi 1 menjadi 2 dan berhasil lagi pada percobaan ulang, meskipun operasinya sudah diterapkan;
  • remove /temp gagal pada eksekusi kedua karena path-nya sudah tidak ada.

Ada tiga cara umum untuk mencapai idempotensi:

Pendekatan Cara kerjanya Paling cocok untuk
Prasyarat versi Operasi pertama adalah test /version, lalu naikkan Resource dengan versi eksplisit
Kunci idempotensi Klien mengirim Idempotency-Key; server menyimpan hasilnya Pembayaran, pesanan, dan penulisan lainnya
Patch keadaan target Gunakan replace bukan add, sehingga hasil hanya bergantung pada nilai akhir Field konfigurasi dan status

Pola terburuk adalah patch yang mencampur add dan remove sambil mengasumsikan percobaan ulang aman. Jika patch menambahkan atau menghapus apa pun, ia harus dipasangkan dengan pernyataan versi atau kunci idempotensi.

6. Konflik Konkuren: ETag dan If-Match

Selain test di dalam patch, HTTP menawarkan mekanisme standar: server mengembalikan ETag, dan klien mengirim If-Match pada request berikutnya.

GET /orders/42 HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
ETag: "a3f9c1"
Content-Type: application/json
PATCH /orders/42 HTTP/1.1
Host: api.example.com
If-Match: "a3f9c1"
Content-Type: application/json-patch+json

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

Jika resource telah berubah, server mengembalikan 412 Precondition Failed, dan klien mengambil ulang sebelum mencoba lagi. If-Match: * berarti "jalankan selama resource ada", yang tidak cocok ketika Anda perlu mencegah penimpaan.

Perbedaan antara test dan If-Match:

  • If-Match diperiksa di lapisan HTTP sebelum body diuraikan, sehingga lebih murah;
  • test diperiksa di lapisan semantik patch dan dapat menyatakan field apa pun tanpa bergantung pada dukungan ETag sisi server;
  • Keduanya dapat digabungkan, tetapi jangan menerapkan hanya salah satunya lalu mengklaim endpoint aman di bawah konkurensi.

7. Perbedaan Library dan Pemilihan

Implementasi Bahasa Atomisitas Dukungan test Catatan
fast-json-patch JavaScript Opsional (secara default mengembalikan dokumen baru) Ya Ekosistem luas; awasi mutateDocument
jsonpatch Python Rollback pada exception Ya Dekat dengan standar, dengan error yang jelas
json-patch Go Beroperasi pada salinan Ya Sering digunakan bersama JSON Merge Patch
zjsonpatch Java Ya Ya Integrasi Jackson yang nyaman

Jangan memilih library hanya berdasarkan "mendukung semua enam operasi". Pastikan tiga hal: apakah kegagalan melakukan rollback, apakah test membandingkan secara ketat, dan apakah add array di luar jangkauan menimbulkan error. Implementasi yang longgar membuat patch yang sama berperilaku berbeda di lingkungan yang berbeda.

8. Daftar Periksa Keamanan Sebelum Rilis

  • [ ] Endpoint mendeklarasikan application/json-patch+json;
  • [ ] Patch diterapkan pada salinan, dan kegagalan tidak pernah mengotori dokumen asli;
  • [ ] Setiap path dan from dalam patch eksternal melewati pemeriksaan allowlist;
  • [ ] Key berbahaya seperti __proto__, constructor, dan prototype ditolak;
  • [ ] Jumlah operasi dan ukuran nilai tunggal dibatasi;
  • [ ] test atau If-Match menyediakan kontrol konkurensi;
  • [ ] Patch ditulis ke audit log dengan pelaku, stempel waktu, dan array operasi lengkap;
  • [ ] Batas transaksi eksplisit ketika patch mencakup beberapa dokumen.

Patch secara inheren adalah deskripsi lengkap "apa yang berubah", yang menjadikannya catatan audit alami. Untuk kode error dan idempotensi di lapisan API, lanjutkan dengan Desain API JSON. Jika patch berasal dari penggabungan dua branch, Anda juga memerlukan semantik konflik, yang dibahas di Penggabungan Git untuk File JSON.

FAQ

Kode status apa yang harus dikembalikan operasi test yang gagal?

Jika konflik berasal dari pernyataan versi, baik 409 Conflict maupun 412 Precondition Failed dapat dipertahankan. Utamakan 412 agar konsisten dengan If-Match, dan sertakan versi saat ini di body respons agar klien dapat mencoba ulang.

Dapatkah patch yang berisi add dan remove dicoba ulang dengan aman?

Tidak secara langsung. add ke akhir array menduplikasi data, dan remove gagal pada eksekusi kedua karena path-nya sudah hilang. Tambahkan prasyarat versi atau kunci idempotensi agar percobaan ulang dikenali sebagai operasi yang sama.

Dapatkah JSON Patch menjamin transaksi lintas dokumen?

Tidak. Atomisitas RFC 6902 berlaku untuk satu dokumen JSON. Ketika pesanan, inventaris, atau resource lain terlibat, gunakan transaksi basis data, kompensasi Saga, atau antrean pesan untuk konsistensi.

Mengapa patch tidak boleh menulis ke __proto__?

Karena sebagian besar implementasi JavaScript menyimpan dokumen dalam objek biasa, menulis __proto__ dapat mengubah rantai prototipe dan memengaruhi setiap objek setelahnya. Allowlist path adalah pertahanan paling andal; jangan hanya mengandalkan penyaringan internal library.

Bagaimana saya membuat request PATCH idempoten?

Ada tiga pendekatan umum: mulai patch dengan test /version dan naikkan versi saat berhasil, buat server menyimpan hasil untuk Idempotency-Key, atau gunakan replace bukan add sehingga hasil hanya bergantung pada nilai akhir. Jika patch menambahkan atau menghapus apa pun, ia harus dipasangkan dengan pernyataan versi atau kunci idempotensi; ia tidak bisa sekadar dicoba ulang.

Haruskah saya menggunakan test dan If-Match keduanya?

Anda tidak harus menggunakan keduanya, tetapi menerapkan hanya salah satunya tidak membuat endpoint aman di bawah konkurensi. If-Match diperiksa di lapisan HTTP sebelum body diuraikan, sehingga lebih murah; test diperiksa di lapisan semantik patch dan dapat menyatakan field apa pun tanpa bergantung pada dukungan ETag sisi server.