JSON Toolkit

JSON Pointer وPatch وMerge Patch

الخلاصة: ثلاث مواصفات قصيرة تشكّل بروتوكول تغيير: Pointer يحدّد الموقع، وPatch يعدّل بدقة، وMerge Patch يدمج جزئيًا. والاختيار الخاطئ يضر بالمصفوفات وبالتزامن.

متوسطقراءة نحو 12 دقيقةنُشر في 2026-10-30حُدِّث في 2026-10-30
غلاف JSON Pointer وPatch وMerge Patch

الخلاصة: استخدم JSON Pointer للتحديد حسب الموضع وJSONPath للترشيح أو العودية؛ واترك إدراج المصفوفات وحذفها وكتابة null وتأكيدات الإصدار لـ JSON Patch، واحفظ Merge Patch للتهيئة المسطحة وللموارد الشبيهة بالنماذج.

التحديد والتعديل أمران مختلفان، وقد قسمتهما المواصفات إلى ثلاثة مستندات. والثمن المعتاد للخلط بينهما هو "استُبدلت المصفوفة بأكملها" دون أن يلاحظ أحد.

1. JSON Pointer: أقصر صياغة للتحديد

يفعل RFC 6901 شيئًا واحدًا فقط: الإشارة إلى عقدة في مستند بسلسلة نصية.

""             → المستند بأكمله
"/store"       → عضو store تحت الجذر
"/store/book"  → مستوى أعمق
"/store/book/0/title" → العنصر 0 في المصفوفة

ثلاث قواعد:

  1. السلسلة الفارغة تشير إلى عقدة الجذر نفسها، لا إلى "عقدة فارغة"؛
  2. كل / يفصل رمزًا مرجعيًا واحدًا؛ وتستخدم المصفوفات فهارس عشرية (ولا يُسمح بالأصفار البادئة، لذا 01 غير صالح)؛
  3. داخل الرمز، تُكتب ~ على صورة ~0 و/ على صورة ~1 — واستبدل ~ قبل /، وإلا فسينتهي بك الأمر إلى تهريب مزدوج.
/**
 * يهرّب رمزًا مرجعيًا واحدًا إلى مقطع في JSON Pointer.
 * @param token اسم المفتاح الأصلي أو الفهرس
 * @returns الرمز بعد التهريب
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * يدمج مصفوفة أسماء مفاتيح في JSON Pointer واحد.
 * @param tokens مصفوفة أسماء المفاتيح أو الفهارس
 * @returns سلسلة Pointer تبدأ بـ /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

تتضح القيمة أكثر عندما يحتوي المفتاح نفسه على /: فمؤشر {"a/b": 1} هو /a~1b، لا /a/b (فالأخير يعني b تحت a).

وعند استخدامه داخل عنوان URL، يلزم أيضًا الترميز بالنسبة المئوية، لأن # و~ لهما معانٍ خاصة في المقطع (fragment).

2. JSON Patch: تسلسل ذرّي من ست عمليات

يصف RFC 6902 التغييرات بـمصفوفة عمليات، بنوع الوسائط 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" }
]
العملية الدلالة الحدود
add تضيف عضوًا أو تدرج عنصرًا في مصفوفة قد يساوي فهرس المصفوفة الطول (إلحاق)؛ و- يعني النهاية
remove تحذف يجب أن يكون المسار موجودًا
replace تستبدل قيمة موجودة يجب أن يكون المسار موجودًا (تكافئ remove + add، لكنها تشترط وجود المسار أولًا)
move تنقل لا يمكنك نقل عقدة تحت مسار فرعي منها
copy تنسخ يجب أن يكون المصدر موجودًا
test تؤكد التساوي إذا فشلت، فلا يُطبَّق التصحيح بأكمله؛ تُستخدم للقفل التفاؤلي

الخاصية الحاسمة هي الذرّية: إذا فشلت أي عملية، يتراجع التصحيح بأكمله، فلا توجد حالة وسيطة "طُبِّقت الثلاث الأولى وفشلت الرابعة". وهذا يجعلها جسم طلب PATCH ممتازًا.

2.1 استخدام test للتزامن التفاؤلي

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

يجري العميل أولًا GET للحصول على etag ويكتبه في عملية test. وعند الكتابات المتزامنة تفشل test في الطلب الثاني، ويعيد الخادم 409 Conflict أو 412 Precondition Failed. وهذا آمن بكثير من قاعدة "آخر كتابة تفوز"، ولا يتطلب قفلًا موزّعًا.

2.2 عمليات المصفوفات هي الأكثر عرضة للأخطاء

// نريد الإدراج عند الفهرس 1
{ "op": "add", "path": "/items/1", "value": {...} }   // صحيح: إدراج؛ والفهرس 1 الأصلي وكل ما بعده يتزحزح للخلف

// نريد استبدال الفهرس 1
{ "op": "replace", "path": "/items/1", "value": {...} } // صحيح: استبدال

عملية add عند فهرس مصفوفة n هي إدراج، لا كتابة فوق قيمة؛ ويجب أن يرفع الفهرس الخارج عن النطاق (n > length) خطأً. بعض التطبيقات متساهلة في ذلك، فيتصرف التصحيح نفسه بطرق مختلفة بين الخوادم.

3. Merge Patch: مقتطف يشبه الهدف

لا يحتوي RFC 7386 إلا على أربع قواعد:

  1. الهدف كائن والتصحيح كائن → دمج متكرر؛
  2. قيمة مفتاح في التصحيح هي null → احذف ذلك المفتاح من الهدف؛
  3. قيمة مفتاح في التصحيح ليست كائنًا → استبدل مباشرة؛
  4. التصحيح نفسه ليس كائنًا → يُستبدل الهدف بأكمله.
// الهدف
{ "a": "b", "c": { "d": "e", "f": "g" } }

// التصحيح
{ "a": "z", "c": { "f": null } }

// النتيجة
{ "a": "z", "c": { "d": "e" } }

نوع الوسائط هو application/merge-patch+json. ويُقرأ كـ"تحديث جزئي"، وهذا ما يجعل إساءة استخدامه سهلة بشكل خاص:

  • لا تُستبدل المصفوفات إلا كاملةً. تريد حذف list[2]؟ لا يستطيع Merge Patch فعل ذلك؛
  • لا يمكنك كتابة null. فـnull محجوز أصلًا لمعنى الحذف؛
  • لا يمكنه التعبير عن حالة ثالثة بين "اترك هذا الحقل كما هو" و"احذف هذا الحقل" (مثل ضبطه صراحة على قيمة فارغة).

4. مصفوفة القدرات: أيّها تختار

المتطلب JSON Patch Merge Patch
استبدال عنصر محدد في مصفوفة بدقة ✓ ✗
حذف عنصر واحد من مصفوفة ✓ ✗
تحديث جزئي لعدة حقول ✓ (عدة replace) ✓ (الأكثر إيجازًا)
كتابة قيمة null ✓ ✗
التحكم في التزامن (تأكيد الإصدار) ✓ (test) ✗ (عبر If-Match فقط)
قابلية قراءة التصحيح متوسطة جيدة (يشبه البيانات نفسها)
تعقيد التنفيذ مرتفع منخفض
أشكال الموارد المناسبة متداخلة بعمق، شبيهة بالقوائم التهيئة المسطحة، النماذج

قواعد عامة:

  • النماذج وصفحات الإعدادات وموارد التهيئة → Merge Patch؛ فالواجهة لا ترسل إلا الحقول المتغيّرة؛
  • الإضافة إلى القوائم والحذف منها، وانتقالات آلة الحالة، وكل ما يحتاج إلى حماية من التزامن → JSON Patch؛
  • يمكن أن يتعايش كليهما: تُعلن نقاط نهاية مختلفة Content-Type مختلفًا، ويوزّع الخادم حسب النوع.

5. ملاحظات التنفيذ

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 افتراضيًا؛ وتعيين mutateDocument إلى true يعدّل في مكانه، فاستخدمه بحذر
const result = applyPatch(document, patch, true, false);
console.log(result.newDocument);

أربع نقاط يسهل إغفالها:

  1. لا تعدّل الكائنات المشتركة في مكانها؛ فالتصحيح الفاشل يترك حالة نصف معدّلة (ما لم يضمن التنفيذ التراجع)؛
  2. التحقق من المسار: يجب أن تتحقق التصحيحات الواردة من الخارج من كل path/from، وترفض مفاتيح مثل __proto__ وconstructor؛
  3. حدود الحجم: ضع سقفًا لعدد مدخلات التصحيح لمنع تصحيح واحد من تنفيذ مئات الآلاف من العمليات؛
  4. التدقيق: التصحيحات مناسبة بطبيعتها لسجلات التدقيق — فهي وصف كامل لـ"ما تغيّر".

6. دلالات الأخطاء

الحالة رمز الحالة المقترح الشرح
التصحيح ليس مصفوفة / غير صالح بنيويًا 400 Bad Request صيغة جسم الطلب خاطئة
العقدة المشار إليها بـ path غير موجودة 422 Unprocessable Entity صحيحة نحويًا لكنها غير قابلة للتنفيذ دلاليًا
فشل عملية test 409 أو 412 يدل على تعارض في الإصدار
لا إذن لتعديل ذلك المسار 403 Forbidden لا تخفِه بـ 404 إلا إذا كنت تخفي وجود المورد عن قصد

التمييز بين 400 و422 مهم: الأول يعني "لا أستطيع فهم تصحيحك"، والثاني يعني "أفهمه، لكن هذه البيانات لا تسمح بذلك التغيير".

7. قائمة التحقق

  • [ ] تُعلن نقطة النهاية Content-Type الصحيح (json-patch+json / merge-patch+json)؛
  • [ ] تطبيق التصحيح ذرّي، ولا يترك الفشل نتيجة جزئية؛
  • [ ] تُتحقق التصحيحات الخارجية مقابل قائمة سماح بالمسارات وحد أقصى للعدد؛
  • [ ] يستخدم التحكم في التزامن test أو If-Match؛
  • [ ] تُكتب التصحيحات في سجل التدقيق؛
  • [ ] تختار سيناريوهات المصفوفات JSON Patch صراحةً؛ ولا تعتمد على Merge Patch.

الأسئلة الشائعة

كيف أقرر بين JSON Patch وMerge Patch؟

استخدم JSON Patch عندما تحتاج إلى استبدال عنصر محدد في مصفوفة أو حذفه، أو كتابة قيمة null، أو تأكيد إصدار؛ واستخدم Merge Patch للنماذج وموارد التهيئة التي لا تحدّث إلا حقولًا قليلة. لا يستطيع Merge Patch حذف عنصر واحد من مصفوفة، ولا كتابة null، لأن null يعني الحذف بالفعل.

لماذا لا تكون add عند فهرس مصفوفة n كتابة فوق القيمة؟

لأن add تدرج في مصفوفة: يتزحزح العنصر عند الفهرس n وكل ما بعده. ولاستبدال عنصر موجود يجب استخدام replace، ويجب أن يكون المسار موجودًا أولًا. ويجب أن يرفع الفهرس الخارج عن النطاق (n > length) خطأً؛ فالتطبيقات المتساهلة تجعل التصحيح نفسه يتصرف بطرق مختلفة بين الخوادم.

هل أعدّل الكائن الأصلي في مكانه؟

لا. فالتصحيح الفاشل يترك حالة نصف معدّلة ما لم يضمن التنفيذ التراجع. طبّق التصحيح على نسخة منسوخة بعمق أو مستنسخة هيكليًا، ثم استبدل المرجع بعد النجاح.