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 را برای پیکربندی مسطح و منابع فرم نگه دارید.

مکان‌یابی و تغییر دو چیز متفاوت‌اند و مشخصات آن‌ها را به سه سند تقسیم کرده. قیمت معمول قاطی کردنشان «کل آرایه جایگزین شد» است و کسی متوجه نمی‌شود.

۱. JSON Pointer: کوتاه‌ترین نحو مکان‌یابی

RFC 6901 دقیقاً یک کار می‌کند: با یک رشته به گره‌ای در سند اشاره می‌کند.

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

سه قاعده:

  1. رشته خالی به خود گره ریشه اشاره می‌کند، نه «گره خالی»؛
  2. هر / یک توکن ارجاع را جدا می‌کند؛ آرایه‌ها از اندیس اعشاری استفاده می‌کنند (صفر پیشرو مجاز نیست، پس 01 نامعتبر است)؛
  3. داخل یک توکن، ~ به‌صورت ~0 و / به‌صورت ~1 نوشته می‌شود — ~ را قبل از / جایگزین کنید، وگرنه escape مضاعف می‌گیرید.
/**
 * Escapes a single reference token into a JSON Pointer segment.
 * @param token Original key name or index
 * @returns Escaped token
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * Joins an array of key names into a JSON Pointer.
 * @param tokens Array of key names or indices
 * @returns Pointer string beginning with /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

ارزشش وقتی بهترین است که خود کلید شامل / باشد: pointer برای {"a/b": 1} برابر /a~1b است، نه /a/b (دومی یعنی b زیر a).

وقتی داخل URL استفاده می‌شود، percent-encoding هم لازم است، چون # و ~ در fragment معنای ویژه دارند.

۲. 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 اعمال نمی‌شود؛ برای قفل خوش‌بینانه

ویژگی کلیدی اتمیک بودن است: اگر هر عملیاتی شکست بخورد، کل patch برمی‌گردد، پس حالت میانی «سه مورد اول اعمال شد و چهارمی شکست خورد» وجود ندارد. این آن را بدنه درخواست PATCH عالی می‌کند.

۲.۱ استفاده از test برای هم‌زمانی خوش‌بینانه

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

کلاینت اول etag را GET می‌کند و در عملیات test می‌نویسد. روی نوشتن هم‌زمان، test درخواست دوم شکست می‌خورد و سرور 409 Conflict یا 412 Precondition Failed برمی‌گرداند. این بسیار امن‌تر از آخرین-نوشتن-برنده است و قفل توزیع‌شده نمی‌خواهد.

۲.۲ عملیات آرایه خطاخیزترین‌اند

// 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 در اندیس آرایه n یک درج است، نه بازنویسی؛ اندیس بیرون از بازه (n > length) باید خطا بدهد. برخی پیاده‌سازی‌ها در این باره سهل‌گیرند، که همان patch را در سرورهای مختلف متفاوت رفتار می‌کند.

۳. Merge Patch: قطعه‌ای که شبیه هدف است

RFC 7386 فقط چهار قاعده دارد:

  1. هدف شیء است و patch شیء است → ادغام بازگشتی؛
  2. مقدار یک کلید در patch برابر null است → آن کلید از هدف حذف می‌شود؛
  3. مقدار یک کلید در patch شیء نیست → مستقیم جایگزین می‌شود؛
  4. خود patch شیء نیست → کل هدف جایگزین می‌شود.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

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

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

نوع رسانه application/merge-patch+json است. شبیه «به‌روزرسانی جزئی» خوانده می‌شود، که سوءاستفاده از آن را مخصوصاً آسان می‌کند:

  • آرایه‌ها فقط یکجا جایگزین می‌شوند. می‌خواهید list[2] را حذف کنید؟ Merge Patch نمی‌تواند؛
  • نمی‌توانید null بنویسید. null از قبل معنای حذف دارد؛
  • نمی‌تواند حالت سومی بین «این فیلد را دست نزن» و «این فیلد را حذف کن» بیان کند (مثلاً صریحاً تنظیم به خالی).

۴. ماتریس توانایی: کدام را انتخاب کنیم

نیاز JSON Patch Merge Patch
جایگزینی دقیق عنصر خاص آرایه ✓ ✗
حذف یک آیتم از آرایه ✓ ✗
به‌روزرسانی جزئی چند فیلد ✓ (چند replace) ✓ (مختصرترین)
نوشتن مقدار null ✓ ✗
کنترل هم‌زمانی (اعلان نسخه) ✓ (test) ✗ (فقط از طریق If-Match)
خوانایی patch متوسط خوب (شبیه خود داده)
پیچیدگی پیاده‌سازی بالا پایین
شکل منابع مناسب عمیقاً تودرتو، فهرست‌مانند پیکربندی مسطح، فرم‌ها

قواعد سرانگشتی:

  • فرم‌ها، صفحه تنظیمات، منابع پیکربندی → Merge Patch؛ فرانت‌اند فقط فیلدهای تغییر‌یافته را ارسال می‌کند؛
  • افزودن و حذف فهرست، گذارهای ماشین حالت، هر چیزی که محافظت هم‌زمانی لازم دارد → JSON Patch؛
  • هر دو می‌توانند هم‌زیستی کنند: endpointهای مختلف Content-Type متفاوت اعلام می‌کنند و سرور بر اساس نوع توزیع می‌کند.

۵. نکات پیاده‌سازی

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);

چهار نکته آسان از دست رفتنی:

  1. اشیای مشترک را درجا دگرگون نکنید؛ patch شکست‌خورده حالت نیمه‌تغییر‌یافته برجا می‌گذارد (مگر پیاده‌سازی rollback را تضمین کند)؛
  2. اعتبارسنجی مسیر: patchهای بیرونی باید هر path/from را اعتبارسنجی و کلیدهایی مانند __proto__ و constructor را رد کنند؛
  3. محدودیت اندازه: تعداد ورودی‌های patch را محدود کنید تا یک patch صدها هزار عملیات انجام ندهد؛
  4. حسابرسی: patchها ذاتاً مناسب لاگ حسابرسی‌اند — توصیف کامل «چه چیزی تغییر کرد».

۶. معناشناسی خطا

موقعیت کد وضعیت پیشنهادی توضیح
patch آرایه نیست / ساختاراً نامعتبر است 400 Bad Request قالب بدنه درخواست اشتباه است
گره ارجاع‌شده توسط path وجود ندارد 422 Unprocessable Entity نحواً معتبر اما معنایی قابل اجرا نیست
عملیات test شکست خورد 409 یا 412 نشان‌دهنده تعارض نسخه
مجوز تغییر آن مسیر وجود ندارد 403 Forbidden با 404 پنهانش نکنید مگر اینکه عمداً وجود منبع را پنهان کنید

تمایز 400 از 422 مهم است: اولی یعنی «patch شما را نمی‌فهمم»، دومی یعنی «می‌فهمم، اما این داده چنین تغییری را مجاز نمی‌داند».

۷. چک‌لیست

  • [ ] endpoint Content-Type درست را اعلام می‌کند (json-patch+json / merge-patch+json)؛
  • [ ] اعمال patch اتمیک است و شکست نتیجه جزئی برجا نمی‌گذارد؛
  • [ ] patchهای بیرونی در برابر فهرست سفید مسیر و محدودیت تعداد ورودی اعتبارسنجی می‌شوند؛
  • [ ] کنترل هم‌زمانی از test یا If-Match استفاده می‌کند؛
  • [ ] patchها در لاگ حسابرسی نوشته می‌شوند؛
  • [ ] سناریوهای آرایه صریحاً JSON Patch را انتخاب می‌کنند؛ به Merge Patch حساب نکنید.

پرسش‌های متداول

چگونه بین JSON Patch و Merge Patch تصمیم بگیرم؟

وقتی باید عنصر خاص آرایه را جایگزین یا حذف کنید، مقدار null بنویسید یا نسخه را اعلان کنید از JSON Patch استفاده کنید؛ برای فرم‌ها و منابع پیکربندی که فقط چند فیلد را به‌روزرسانی می‌کنند از Merge Patch. Merge Patch نمی‌تواند یک آیتم آرایه را حذف کند و نمی‌تواند null بنویسد، چون null از قبل معنای حذف دارد.

چرا add در اندیس آرایه n بازنویسی نیست؟

چون add در آرایه درج می‌کند: عنصر اندیس n و همه بعد از آن عقب می‌روند. برای جایگزینی عنصر موجود باید replace استفاده کنید و مسیر اول باید وجود داشته باشد. اندیس بیرون از بازه (n > length) باید خطا بدهد؛ پیاده‌سازی‌های سهل‌گیر همان patch را در سرورهای مختلف متفاوت رفتار می‌کنند.

آیا باید شیء اصلی را درجا تغییر دهم؟

نه. patch شکست‌خورده حالت نیمه‌تغییر‌یافته برجا می‌گذارد مگر پیاده‌سازی rollback را تضمین کند. patch را روی کپی عمیق یا structured-clone اعمال کنید، سپس مرجع را پس از موفقیت جابه‌جا کنید.