JSON Toolkit

JSON Patch در محیط تولید: اتمیک بودن، idempotency و تعارض‌های هم‌زمانی

جمع‌بندی: JSON Patch فقط یک قالب تغییر است؛ اتمیک بودن، idempotency و هم‌زمانی همچنان کار شماست. پیش‌شرط‌های test، escape مسیر، rollback و هماهنگی ETag را پوشش می‌دهد.

پیشرفتهحدود 10 دقیقه مطالعهانتشار 2026-10-31به‌روزرسانی 2026-10-31
تصویر جلد JSON Patch در محیط تولید

جمع‌بندی: اول مسیرهای هدف و مجوزهای patch را اعتبارسنجی کنید، سپس با test یا If-Match اعلان هم‌زمانی اضافه کنید؛ patch را روی یک کپی اعمال کنید تا شکست چیزی نیمه‌نوشته باقی نگذارد.

خطر واقعی درخواست PATCH نوشتن فیلد اشتباه نیست. این است که باور کنید نوشتن موفق شد در حالی که بی‌صدا تغییر شخص دیگری را بازنویسی کرده‌اید. JSON Patch ابزارها را می‌دهد، اما طراحی ایمنی همچنان کار شماست.

۱. مرور سریع مجموعه عملیات RFC 6902

JSON Patch تغییرات را به‌صورت آرایه‌ای از عملیات توصیف می‌کند، با نوع رسانه application/json-patch+json. شش عملیات به ترتیب آرایه اجرا می‌شوند:

عملیات اثر قید کلیدی
add افزودن عضو یا درج عنصر آرایه اندیس آرایه می‌تواند برابر طول باشد؛ - یعنی انتها
remove حذف عضو یا عنصر مسیر هدف باید وجود داشته باشد
replace جایگزینی مقدار موجود مسیر هدف باید وجود داشته باشد
move جابه‌جایی گره نمی‌توان گره را به مسیر فرزند خودش برد
copy کپی گره مسیر منبع باید وجود داشته باشد
test اعلان برابری مقدار اگر شکست بخورد، کل patch اعمال نمی‌شود

بسیاری فقط سه مورد اول را به یاد می‌آورند و فراموش می‌کنند test همان چیزی است که نوشتن کورکورانه را به نوشتن شرطی تبدیل می‌کند. معناشناسی کامل عملیات و مرزهای آرایه در JSON Pointer، Patch و Merge Patch پوشش داده شده است.

۲. استفاده از test برای تبدیل patch به به‌روزرسانی شرطی

یک patch معمول قفل خوش‌بینانه با نوشتن شروع نمی‌شود، با اعلان نسخه شروع می‌شود:

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

در patch کامل بگذارید:

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

منطق: تأیید کن نسخه سند اکنون ۱ است، سپس به ۲ تغییرش بده. اگر درخواست دیگری از قبل نسخه را به ۲ رسانده باشد، test اول شکست می‌خورد، کل patch اعمال نمی‌شود و سرور تعارض برمی‌گرداند. این از به‌روزرسانی گم‌شده جلوگیری می‌کند بدون معرفی قفل توزیع‌شده.

چند نکته طراحی:

  • test را پیش از هر نوشتن بگذارید. اگر نوشتن قبلی از قبل اثر کرده، اعلان بی‌معناست؛
  • روی شماره نسخه یا digest تولیدشده توسط سرور اعلان کنید، نه فیلد معمولی که کلاینت آزادانه تغییر می‌دهد؛
  • نسخه را روی هر نوشتن موفق افزایش دهید؛ وگرنه دو درخواست با همان نسخه قدیمی هر دو پاس می‌شوند؛
  • value در test باید دقیقاً همان نوع JSON مقدار سند را داشته باشد. 1 و "1" برابر نیستند.

۳. escape مسیر و آلودگی prototype

قواعد escape JSON Pointer ساده است: ~ را ~0 و / را ~1 بنویسید، و ~ را قبل از / جایگزین کنید، وگرنه escape مضاعف می‌گیرید.

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

ریسک patchهای بیرونی معمولاً escape نیست، کلیدهای خطرناک است. در جاوااسکریپت، کلیدهایی مانند __proto__، constructor و prototype می‌توانند آلودگی prototype تحریک کنند، حتی از طریق add معمولی:

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

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

  1. اجازه دهید patch فقط فیلدهای کسب‌وکار را تغییر دهد، مانند /status یا /items/0/quantity؛
  2. هر path و from را در برابر عبارت باقاعده یا پیشوند مسیر اعتبارسنجی کنید و سطوح غیرمنتظره غیر از اندیس آرایه را رد کنید؛
  3. JSON را به ظرف‌های Object.create(null) یا Map تجزیه کنید تا اثر زنجیره prototype آلوده کم شود؛
  4. تعداد عملیات و اندازه بایتی هر value را محدود کنید.

۴. اعمال اتمیک و rollback

RFC 6902 اجرای اتمیک می‌خواهد، اما اتمیک بودن یک الزام معنایی است، نه پیش‌فرض هر کتابخانه. کتابخانه‌هایی مانند fast-json-patch وقتی mutateDocument برابر true است سند را درجا دگرگون می‌کنند و شکست ممکن است تغییرات جزئی برجا بگذارد.

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

/**
 * Apply a patch immutably and return the original document on failure.
 * @param document The original document, which callers must not mutate
 * @param patch The JSON Patch operation array to apply
 * @returns The result, including a success flag and the new document
 */
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 };
  }
}

سه توصیه پیاده‌سازی:

  • اول کپی عمیق یا structured-clone کنید، روی کپی اعمال کنید و مرجع را فقط پس از موفقیت جابه‌جا کنید؛
  • به rollback خودکار کتابخانه تکیه نکنید؛ رفتار روی استثنا بین پیاده‌سازی‌ها متفاوت است؛
  • پیش از اجرا اعتبارسنجی کنید: validate فقط ساختار را بررسی می‌کند، در حالی که تعارض‌های معنایی همچنان test می‌خواهند؛ هیچ‌کدام جای دیگری را نمی‌گیرد.

اگر patch چند منبع را در بر می‌گیرد، مثلاً تغییر هم‌زمان سفارش و موجودی‌اش، اتمیک بودن JSON Patch فقط داخل یک سند برقرار است. در بین اسناد به تراکنش پایگاه‌داده یا اقدامات جبرانی نیاز دارید.

۵. Idempotency: PATCH به‌طور طبیعی idempotent نیست

معناشناسی HTTP PUT و DELETE را idempotent تعریف می‌کند، اما PATCH لزوماً چنین نیست. تکرار همان JSON Patch می‌تواند نتایج کاملاً متفاوتی بدهد:

  • add /items/- هر بار به انتهای آرایه ضمیمه می‌کند، پس تلاش مجدد تکراری می‌سازد؛
  • replace /version نسخه ۱ را به ۲ تغییر می‌دهد و در تلاش مجدد دوباره موفق می‌شود، حتی اگر عملیات از قبل اعمال شده باشد؛
  • remove /temp در اجرای دوم شکست می‌خورد چون مسیر دیگر وجود ندارد.

سه راه رایج برای دستیابی به idempotency:

رویکرد چگونه کار می‌کند مناسب برای
پیش‌شرط نسخه عملیات اول test /version، سپس افزایش منابع با نسخه صریح
کلید idempotency کلاینت Idempotency-Key می‌فرستد؛ سرور نتیجه را کش می‌کند پرداخت، سفارش و نوشتن‌های دیگر
patch حالت هدف از replace به‌جای add استفاده کنید تا نتیجه فقط به مقدار نهایی بستگی داشته باشد فیلدهای پیکربندی و وضعیت

بدترین الگو patchی است که add و remove را مخلوط می‌کند و تلاش مجدد را ایمن فرض می‌کند. اگر patch چیزی ضمیمه یا حذف می‌کند، باید با اعلان نسخه یا کلید idempotency جفت شود.

۶. تعارض‌های هم‌زمانی: ETag و If-Match

علاوه بر test داخل patch، 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 در لایه معناشناسی patch بررسی می‌شود و می‌تواند هر فیلدی را بدون وابستگی به پشتیبانی ETag سرور اعلان کند؛
  • می‌توانند ترکیب شوند، اما فقط یکی را پیاده نکنید و ادعا کنید endpoint زیر هم‌زمانی امن است.

۷. تفاوت کتابخانه‌ها و انتخاب

پیاده‌سازی زبان اتمیک بودن پشتیبانی test یادداشت
fast-json-patch JavaScript اختیاری (به‌صورت پیش‌فرض سند جدید برمی‌گرداند) بله اکوسیستم گسترده؛ به mutateDocument توجه کنید
jsonpatch Python روی استثنا rollback می‌کند بله نزدیک به استاندارد، با خطاهای واضح
json-patch Go روی کپی کار می‌کند بله غالباً همراه JSON Merge Patch استفاده می‌شود
zjsonpatch Java بله بله یکپارچه‌سازی راحت با Jackson

کتابخانه را فقط بر اساس «همه شش عملیات را پشتیبانی می‌کند» انتخاب نکنید. سه چیز را تأیید کنید: آیا شکست rollback می‌کند، آیا test سخت‌گیرانه مقایسه می‌کند و آیا add آرایه با اندیس بیرون از بازه خطا می‌دهد. پیاده‌سازی‌های سهل‌گیر همان patch را در محیط‌های مختلف متفاوت رفتار می‌کنند.

۸. چک‌لیست ایمنی پیش از انتشار

  • [ ] endpoint نوع application/json-patch+json را اعلام می‌کند؛
  • [ ] patchها روی کپی اعمال می‌شوند و شکست هرگز سند اصلی را آلوده نمی‌کند؛
  • [ ] هر path و from در patchهای بیرونی از بررسی فهرست سفید عبور می‌کند؛
  • [ ] کلیدهای خطرناک مانند __proto__، constructor و prototype رد می‌شوند؛
  • [ ] تعداد عملیات و اندازه مقدار واحد محدود است؛
  • [ ] test یا If-Match کنترل هم‌زمانی می‌دهد؛
  • [ ] patchها با کنشگر، مهر زمانی و آرایه کامل عملیات در لاگ حسابرسی نوشته می‌شوند؛
  • [ ] مرزهای تراکنش وقتی patch چند سند را در بر می‌گیرد صریح‌اند.

patch ذاتاً توصیف کامل «چه چیزی تغییر کرد» است، که آن را رکورد حسابرسی طبیعی می‌کند. برای کدهای خطا و idempotency در لایه API، طراحی API در JSON را ادامه دهید. اگر patch از ادغام دو شاخه می‌آید، به معناشناسی تعارض هم نیاز دارید که در ادغام Git برای فایل‌های JSON پوشش داده شده است.

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

عملیات test شکست‌خورده چه کد وضعیتی برگرداند؟

اگر تعارض از اعلان نسخه می‌آید، هم 409 Conflict و هم 412 Precondition Failed قابل دفاع‌اند. 412 را برای هم‌خوانی با If-Match ترجیح دهید و نسخه فعلی را در بدنه پاسخ بگذارید تا کلاینت بتواند تلاش مجدد کند.

آیا patchی حاوی add و remove می‌تواند ایمن تکرار شود؟

مستقیم نه. add به انتهای آرایه داده را تکراری می‌کند و remove در اجرای دوم شکست می‌خورد چون مسیر رفته است. پیش‌شرط نسخه یا کلید idempotency اضافه کنید تا تلاش‌های مجدد به‌عنوان همان عملیات شناخته شوند.

آیا JSON Patch می‌تواند تراکنش بین اسناد را تضمین کند؟

نه. اتمیک بودن RFC 6902 به یک سند JSON واحد اعمال می‌شود. وقتی سفارش، موجودی یا منابع دیگر دخیل‌اند، از تراکنش پایگاه‌داده، جبران Saga یا صف پیام برای یکدستی استفاده کنید.

چرا patch هرگز نباید به __proto__ بنویسد؟

چون بیشتر پیاده‌سازی‌های جاوااسکریپت سند را در اشیای معمولی نگه می‌دارند، نوشتن __proto__ می‌تواند زنجیره prototype را تغییر دهد و روی هر شیء بعدی اثر بگذارد. فهرست سفید مسیر قابل اعتمادترین دفاع است؛ فقط به فیلتر داخلی کتابخانه تکیه نکنید.

چگونه یک درخواست PATCH را idempotent کنم؟

سه رویکرد رایج: patch را با test /version شروع کنید و نسخه را روی موفقیت افزایش دهید، سرور نتیجه را برای Idempotency-Key کش کند، یا از replace به‌جای add استفاده کنید تا نتیجه فقط به مقدار نهایی بستگی داشته باشد. اگر patch چیزی ضمیمه یا حذف می‌کند، باید با اعلان نسخه یا کلید idempotency جفت شود؛ نمی‌توان صرفاً تکرارش کرد.

آیا باید هم test و هم If-Match استفاده کنم؟

لازم نیست هر دو را استفاده کنید، اما پیاده کردن فقط یکی endpoint را زیر هم‌زمانی امن نمی‌کند. If-Match در لایه HTTP پیش از تجزیه بدنه بررسی می‌شود، پس ارزان‌تر است؛ test در لایه معناشناسی patch بررسی می‌شود و می‌تواند هر فیلدی را بدون وابستگی به پشتیبانی ETag سرور اعلان کند.