JSON Patch في بيئة الإنتاج: الذرّية والتكرار الآمن والتعارضات المتزامنة
الخلاصة: JSON Patch مجرد صيغة تغيير؛ فالذرّية والتكرار الآمن والتعارضات المتزامنة تظل مسؤوليتك أنت. يغطي المقال شروط test المسبقة، وتهريب المسارات، والتراجع، والتنسيق عبر ETag.

الخلاصة: تحقّق من مسارات هدف التصحيح وأذوناته أولًا، ثم أضف تحققًا من التزامن بـ test أو If-Match؛ وطبّق التصحيح على نسخة بحيث لا يترك الفشل شيئًا مكتوبًا في منتصفه.
الخطر الحقيقي في طلب
PATCHليس كتابة الحقل الخطأ. بل الاعتقاد بأن الكتابة نجحت بينما هي تطمس تغيير شخص آخر بصمت. يمنحك JSON Patch الأدوات، لكن تصميم الأمان يظل مسؤوليتك.
1. مراجعة سريعة لمجموعة عمليات RFC 6902
يصف JSON Patch التغييرات كمصفوفة عمليات، بنوع الوسائط application/json-patch+json. وتُنفَّذ ست عمليات بترتيب المصفوفة:
| العملية | التأثير | القيد الأساسي |
|---|---|---|
add |
إضافة عضو أو إدراج عنصر في مصفوفة | قد يساوي فهرس المصفوفة الطول؛ و- يعني النهاية |
remove |
حذف عضو أو عنصر | يجب أن يكون مسار الهدف موجودًا |
replace |
استبدال قيمة موجودة | يجب أن يكون مسار الهدف موجودًا |
move |
نقل عقدة | لا يمكن نقل عقدة إلى مسار أحد أبنائها |
copy |
نسخ عقدة | يجب أن يكون مسار المصدر موجودًا |
test |
تأكيد تساوي القيمتين | إذا فشل، فلا يُطبَّق التصحيح بأكمله |
كثيرون يتذكرون الثلاث الأولى فقط وينسون أن test هي ما يحوّل الكتابة العمياء إلى كتابة شرطية. أما دلالات العمليات الكاملة وحدود المصفوفات فقد غُطيت في JSON Pointer وPatch وMerge Patch.
2. استخدام test لتحويل التصحيح إلى تحديث شرطي
يبدأ تصحيح القفل التفاؤلي النمطي ليس بكتابة، بل بتأكيد على رقم الإصدار:
{"op":"test","path":"/version","value":1}
ضعها في تصحيح كامل:
[
{ "op": "test", "path": "/version", "value": 1 },
{ "op": "replace", "path": "/version", "value": 2 },
{ "op": "replace", "path": "/status", "value": "paid" }
]
المنطق هو: تأكَّد أن إصدار المستند حاليًا 1، ثم غيّره إلى 2. فإذا كان طلب آخر قد رفع الإصدار إلى 2 بالفعل، فسيفشل test الأول، ولا يُطبَّق التصحيح بأكمله، ويعيد الخادم تعارضًا. وهذا يمنع فقدان التحديثات دون إدخال قفل موزّع.
وفيما يلي نقاط تصميمية:
- ضع
testقبل كل كتابة. فإذا كانت كتابة سابقة قد نفّذت بالفعل، صار التأكيد بلا معنى؛ - أكّد على رقم إصدار أو بصمة يولّدها الخادم، لا على حقل عادي يستطيع العميل تغييره بحرية؛
- زِد الإصدار عند كل كتابة ناجحة؛ وإلا فسيمرّ طلبان يستخدمان الإصدار القديم نفسه؛
- يجب أن تكون قيمة
valueفيtestمن نوع JSON مطابق تمامًا لقيمة المستند. فـ1و"1"غير متساويين.
3. تهريب المسارات وتلويث النموذج الأولي
قواعد التهريب في JSON Pointer بسيطة: اكتب ~ على صورة ~0 و/ على صورة ~1، واستبدل ~ قبل /، وإلا فستحصل على تهريب مزدوج.
المفتاح "a/b" → /a~1b
المفتاح "m~n" → /m~0n
المفتاح "a~/b" → /a~0~1b
الخطر في التصحيحات الخارجية ليس التهريب عادةً، بل المفاتيح الخطرة. ففي JavaScript، يمكن لمفاتيح مثل __proto__ وconstructor وprototype أن تسبب تلويث النموذج الأولي، حتى عبر عملية add عادية:
[
{ "op": "add", "path": "/__proto__/polluted", "value": true }
]
استخدم قائمة سماح بدلًا من قائمة منع:
- اسمح للتصحيحات بتعديل الحقول التجارية فقط، مثل
/statusأو/items/0/quantity؛ - تحقّق من كل
pathوfromمقابل تعبير نمطي أو بادئة مسار، وارفض المستويات غير المتوقعة غير فهارس المصفوفات؛ - حلّل JSON في حاويات
Object.create(null)أوMapلتقليل أثر سلسلة النموذج الملوّثة؛ - ضع حدًّا أعلى لعدد العمليات وحجم البايتات لكل
value.
4. التطبيق الذرّي والتراجع
يشترط RFC 6902 التنفيذ الذرّي، لكن الذرّية متطلب دلالي لا سلوك افتراضي في كل مكتبة. فمكتبات مثل fast-json-patch تعدّل المستند في مكانه عندما تكون mutateDocument مساوية لـ true، وقد يترك الفشل تغييرات جزئية خلفه.
import { applyPatch, validate } from 'fast-json-patch';
/**
* يطبّق تصحيحًا بطريقة غير قابلة للتغيير ويعيد المستند الأصلي عند الفشل.
* @param document المستند الأصلي، ويجب ألا يعدّله المستدعي
* @param patch مصفوفة عمليات JSON Patch المراد تطبيقها
* @returns النتيجة، وتتضمن علم النجاح والمستند الجديد
*/
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 };
}
}
ثلاث توصيات على مستوى التنفيذ:
- انسخ نسخة عميقة أو استنساخًا هيكليًا أولًا، وطبّق على النسخة، واستبدل المرجع بعد النجاح فقط؛
- لا تعتمد على التراجع التلقائي في المكتبة؛ فسلوك الاستثناءات يختلف بين التطبيقات؛
- تحقّق قبل التنفيذ: يفحص
validateالبنية فقط، أما التعارضات الدلالية فتظل تحتاج إلىtest؛ ولا يغني أحدهما عن الآخر.
إذا كان التصحيح يمتد عبر موارد متعددة، مثل تغيير طلب ومخزونه معًا، فإن ذرّية JSON Patch لا تتحقق إلا داخل مستند واحد. وأما عبر المستندات فتحتاج إلى معاملة قاعدة بيانات أو إجراءات تعويضية.
5. التكرار الآمن: PATCH ليست متكررة بأمان بطبيعتها
تعرف دلالات HTTP كلا من PUT وDELETE بأنهما متكرران بأمان، لكن PATCH ليس كذلك بالضرورة. فتكرار JSON Patch نفسه قد ينتج نتائج مختلفة تمامًا:
add /items/-يضيف إلى نهاية المصفوفة في كل مرة، فتخلق إعادة المحاولة عناصر مكررة؛replace /versionيغيّر الإصدار من 1 إلى 2 وينجح مرة أخرى عند إعادة المحاولة، رغم أن العملية نُفِّذت بالفعل؛remove /tempيفشل في التشغيل الثاني لأن المسار لم يعد موجودًا.
وهناك ثلاث طرق شائعة لتحقيق التكرار الآمن:
| النهج | كيف يعمل | الأنسب لـ |
|---|---|---|
| شرط الإصدار المسبق | العملية الأولى test /version، ثم زيادة الإصدار |
الموارد ذات إصدار صريح |
| مفتاح التكرار الآمن | يرسل العميل Idempotency-Key؛ ويخزّن الخادم النتيجة |
المدفوعات والطلبات وغيرها من الكتابات |
| تصحيح حالة الهدف | استخدام replace بدل add، فتكون النتيجة معتمدة على القيمة النهائية فقط |
حقول التهيئة والحالة |
أسوأ نمط هو تصحيح يخلط add وremove مع افتراض أن إعادة المحاولة آمنة. فإذا كان التصحيح يضيف أو يحذف أي شيء، فيجب أن يقترن بتأكيد على الإصدار أو بمفتاح تكرار آمن.
6. التعارضات المتزامنة: ETag وIf-Match
إلى جانب test داخل التصحيح، توفّر HTTP آلية معيارية: يعيد الخادم ETag، ويرسل العميل If-Match في الطلب التالي.
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"}]
إذا تغيّر المورد، يعيد الخادم 412 Precondition Failed، ويعيد العميل الجلب قبل إعادة المحاولة. وتعني If-Match: * "نفّذ ما دام المورد موجودًا"، وهي غير مناسبة عندما تحتاج إلى منع الطمس.
الفرق بين test وIf-Match:
- يُفحص
If-Matchفي طبقة HTTP قبل تحليل الجسم، لذا فهو أرخص؛ - يُفحص
testفي طبقة دلالات التصحيح ويمكنه تأكيد أي حقل دون الاعتماد على دعم ETag في الخادم؛ - يمكن الجمع بينهما، لكن لا تنفّذ واحدًا فقط وتزعم أن نقطة النهاية آمنة في ظل التزامن.
7. فروق المكتبات واختيارها
| التطبيق | اللغة | الذرّية | دعم test |
ملاحظات |
|---|---|---|---|---|
fast-json-patch |
JavaScript | اختيارية (يعيد مستندًا جديدًا افتراضيًا) | نعم | نظام بيئي واسع؛ احترس من mutateDocument |
jsonpatch |
Python | يتراجع عند الاستثناءات | نعم | قريب من المعيار، بأخطاء واضحة |
json-patch |
Go | يعمل على نسخة | نعم | يُستخدم غالبًا مع JSON Merge Patch |
zjsonpatch |
Java | نعم | نعم | تكامل مريح مع Jackson |
لا تختَر مكتبة بناءً على "تدعم العمليات الست جميعها" فقط. تأكّد من ثلاثة أمور: هل يتراجع الفشل، وهل يقارن test بشكل صارم، وهل ترفع add على فهرس مصفوفة خارج النطاق خطأً. فالتطبيقات المتساهلة تجعل التصحيح نفسه يتصرف بطرق مختلفة بين البيئات.
8. قائمة تحقق أمان قبل الإطلاق
- [ ] تُعلن نقطة النهاية
application/json-patch+json؛ - [ ] تُطبَّق التصحيحات على نسخة، ولا يلوّث الفشل المستند الأصلي أبدًا؛
- [ ] يمرّ كل
pathوfromفي التصحيحات الخارجية عبر فحص قائمة السماح؛ - [ ] تُرفض المفاتيح الخطرة مثل
__proto__وconstructorوprototype؛ - [ ] عدد العمليات وحجم القيمة الواحدة محدودان؛
- [ ] يوفّر
testأوIf-Matchتحكمًا في التزامن؛ - [ ] تُكتب التصحيحات في سجل تدقيق مع الفاعل والطابع الزمني ومصفوفة العمليات الكاملة؛
- [ ] حدود المعاملات صريحة عندما يمتد التصحيح عبر مستندات.
التصحيح هو وصف كامل لـ"ما تغيّر"، وهو ما يجعله سجل تدقيق طبيعيًا. أما رموز الأخطاء والتكرار الآمن على مستوى API، فتابع بها في تصميم JSON API. وإذا كان التصحيح ناتجًا عن دمج فرعين، فأنت تحتاج أيضًا إلى دلالات التعارض، وهو ما يُغطّى في دمج Git لملفات JSON.
الأسئلة الشائعة
أي رمز حالة يجب أن تعيده عملية test فاشلة؟
إذا كان التعارض ناتجًا عن تأكيد على الإصدار، فكلٌّ من 409 Conflict و412 Precondition Failed مقبول بتبرير. فضّل 412 للبقاء متسقًا مع If-Match، وأدرج الإصدار الحالي في جسم الاستجابة حتى يستطيع العميل إعادة المحاولة.
هل يمكن إعادة محاولة تصحيح يحتوي add وremove بأمان؟
ليس مباشرة. فـadd إلى نهاية مصفوفة يكرر البيانات، وremove يفشل في التشغيل الثاني لأن المسار اختفى. أضف شرط إصدار مسبق أو مفتاح تكرار آمن بحيث تُعرَف إعادة المحاولة كالعملية نفسها.
هل يضمن JSON Patch معاملات عبر المستندات؟
لا. تنطبق ذرّية RFC 6902 على مستند JSON واحد. وعندما تكون الطلبات أو المخزون أو غيرها من الموارد معنية، استخدم معاملات قاعدة البيانات أو تعويض Saga أو قائمة رسائل للاتساق.
لماذا يجب ألا يكتب التصحيح إلى __proto__ أبدًا؟
لأن معظم تطبيقات JavaScript تخزّن المستند في كائنات عادية، وكتابة __proto__ قد تغيّر سلسلة النموذج الأولي وتؤثر في كل كائن بعد ذلك. وقائمة السماح بالمسارات هي أكثر دفاع موثوق؛ فلا تعتمد فقط على التصفية الداخلية للمكتبة.
كيف أجعل طلب PATCH متكررًا بأمان؟
هناك ثلاث طرق شائعة: ابدأ التصحيح بـ test /version وزِد الإصدار عند النجاح، أو أن يخزّن الخادم النتيجة عند Idempotency-Key، أو استخدام replace بدل add بحيث تعتمد النتيجة على القيمة النهائية فقط. وإذا كان التصحيح يضيف أو يحذف أي شيء، فيجب أن يقترن بتأكيد على الإصدار أو بمفتاح تكرار آمن؛ ولا يصح إعادة محاولته ببساطة.
هل أستخدم test وIf-Match معًا؟
ليس عليك استخدام كليهما، لكن تنفيذ واحد فقط لا يجعل نقطة النهاية آمنة في ظل التزامن. يُفحص If-Match في طبقة HTTP قبل تحليل الجسم، لذا فهو أرخص؛ ويُفحص test في طبقة دلالات التصحيح ويمكنه تأكيد أي حقل دون الاعتماد على دعم ETag في الخادم.