پرسشهای متداول پرسوجو و اعتبارسنجی: JSONPath و Schema
جمعبندی: آیا پرسوجوی مسیر باید از JSONPath یا Pointer استفاده کند، چگونه بین Patch و Merge Patch انتخاب کنیم، Schema چه چیزی را میتواند مسدود کند، و نسخه draft را چگونه انتخاب کنیم — همه در یک مرور توضیح داده شده است.

جمعبندی: برای یافتن گرهها بر اساس شرط از JSONPath استفاده کنید، برای آدرسدهی و ویرایش مکان دقیق از JSON Pointer یا JSON Patch، و برای ادغام پیکربندی از Merge Patch؛ Schemaهای جدید را به 2020-12 سنجاق کنید و allErrors را روشن کنید تا همه شکستها در یک مرور برگردند.
پرسوجو یعنی «پیدا کردنش»؛ اعتبارسنجی یعنی «تأیید درستیاش». هرکدام استاندارد خود را دارند، پس آنها را با هم مخلوط نکنید.
۱. پرسوجوی مسیر
چگونه بین JSONPath و JSON Pointer انتخاب کنم؟
بستگی دارد به اینکه «بر اساس شرط پیدا میکنید» یا «بر اساس موقعیت میگیرید».
| بُعد | JSONPath | JSON Pointer |
|---|---|---|
| استاندارد | RFC 9535 (۲۰۲۴) | RFC 6901 (۲۰۱۳) |
| عبارت | $.users[?(@.age > 18)].id |
/users/0/id |
| نویسه عمومی و بازگشت | از *، .. پشتیبانی میکند |
پشتیبانی نمیشود |
| فیلتر شرطی | پشتیبانی میشود | پشتیبانی نمیشود |
| نتیجه | مجموعه گره (احتمالاً چندتایی) | یک مکان واحد |
| بیانگری | قوی، اما با میراث سنگین تفاوتهای پیادهسازی | ضعیف، اما کاملاً قطعی |
| استفاده معمول | استخراج لاگ، عیبیابی، کاوش داده | JSON Patch، آدرسدهی پیکربندی، مسیر خطا |
قاعده انتخاب سرراست است:
- باید «همه گرههای مطابق یک شرط را بگیرید» → JSONPath؛
- باید «یک مکان دقیق را سنجاق کنید و آن را اضافه، حذف یا تغییر دهید» → JSON Pointer؛
- با هم استفاده کنید: گره را با JSONPath پیدا کنید و مکانش را با Pointer توصیف کنید.
از JSONPath برای آدرسدهی در قرارداد API استفاده نکنید. JSONPath مجموعه برمیگرداند و سه شاخه نتیجه خالی، تکعنصری و چندعنصری پروتکل را پیچیده میکند؛ Pointer یعنی «یا به مکانی موجود اشاره میکند یا وجود ندارد»، که برای قرارداد مناسبتر است.
تفاوت $.. و $.* چیست؟
$.. نزول بازگشتی است و همه سطوح را طی میکند؛ $.* فقط فرزندان مستقیم سطح جاری را برمیگرداند.
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ دو شیء{id:1, child:{...}}و{id:3}؛$..id→ سه مقدار:1،2،3؛$.*.id→1و3، بدون2تودرتو.
نزول بازگشتی دستی است اما گران: $.. کل درخت را طی میکند که روی سند بزرگ O(n) است و بهراحتی فیلدهای عمیقی را میگیرد که قصدشان را نداشتید.
اگر سطح مشخصی کار میکند، از .. پرهیز کنید.
آیا فیلترها میتوانند رشته مقایسه کنند؟ آیا با regex تطبیق میدهند؟
مقایسه رشته کار میکند؛ regex به پیادهسازی بستگی دارد.
نحو استاندارد (RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // compare with another node
مقایسه رشته از گیومه دوتایی استفاده میکند (تکگیومه هم در RFC 9535 مجاز است، اما برخی پیادهسازیهای قدیمیتر فقط گیومه دوتایی را میپذیرند).
مقایسه عددی از معناشناسی عددی پیروی میکند؛ مخلوط کردن رشته با عدد، مانند "10" > 9، در استاندارد رفتار تعریفنشده است، پس به آن تکیه نکنید.
برای regex، RFC 9535 توابع match() و search() را تعریف میکند، اما پشتیبانی از آنها بین پیادهسازیها بسیار متفاوت است.
رویکرد امن: بگذارید JSONPath فیلتر درشت انجام دهد و تطبیق regex را پس از دریافت نتایج در کد انجام دهید.
از JSON Patch استفاده کنم یا Merge Patch؟
هر دو توصیف میکنند «چگونه چیزی را تغییر دهیم»، اما بیانگری و معناشناسی آنها کاملاً متفاوت است.
| بُعد | JSON Patch (RFC 6902) | Merge Patch (RFC 7386) |
|---|---|---|
| شکل | آرایهای از عملیات [{"op":"add",...}] |
شیء جزئی |
| عملیات پشتیبانیشده | add / remove / replace / move / copy / test | فقط ادغام (حذف با null) |
| آدرسدهی به عناصر آرایه | بله، با اندیس | نه، کل آرایه جایگزین میشود |
| اتمیک بودن | test قفل خوشبینانه را ممکن میکند |
هیچ |
| اندازه سند | بزرگ اما دقیق | کوچک اما محدود در بیانگری |
| مناسب برای | ویرایش ریزدانه، بهروزرسانی جزئی API | ادغام پیکربندی، ارسال جزئی فرم |
معیار تصمیم:
- تغییر یک فیلد از عنصر سوم یک آرایه → JSON Patch؛
- ادغام چند ورودی پیکربندی در پیکربندی موجود → Merge Patch؛
- کنترل همزمانی «اگر مقدار اصلی X نبود شکست بخور» →
testدر JSON Patch.
به تله معناشناسی null در Merge Patch توجه کنید: null یعنی «این کلید را حذف کن»، نه «مقدار را null کن».
تنظیم مقدار به null با Merge Patch ممکن نیست.
۲. اعتبارسنجی Schema
JSON Schema چه مشکلاتی را حل میکند؟
«داده باید چگونه باشد» را از توافق شفاهی به قراردادی تبدیل میکند که ماشین میتواند بررسی کند. خطاهایی که واقعاً میتواند مسدود کند:
- فیلدهای الزامی گمشده (
required)؛ - نوع اشتباه، مثلاً رشتهای که عدد انتظار میرود (
type)؛ - مقادیر بیرون از بازه (
minimum/maximum/minLength)؛ - مقادیر بیرون از مجموعه مجاز (
enum)؛ - نوع ناسازگار عناصر آرایه یا طول اشتباه آرایه (
items/minItems)؛ - فیلدهای گمشده در ساختارهای تودرتو (اعتبارسنجی بازگشتی از طریق
properties)؛ - فیلدهای اضافی (
additionalProperties: false).
چیزی که نمیتواند حل کند: قواعد کسبوکار («زمان پایان باید بعد از زمان شروع باشد»)، شرطهای بینفیلدی (برخی با if/then
قابل بیاناند اما خوانایی آسیب میبیند)، یکتایی پایگاهداده و بررسی مجوز.
پس Schema دروازهبان «شکل داده» است، نه جایگزین منطق کسبوکار.
از draft-07 استفاده کنم یا 2020-12؟
برای پروژههای جدید از 2020-12 استفاده کنید؛ برای سیستمهای موجود ببینید اعتبارسنج از چه چیزی پشتیبانی میکند.
| نسخه | وضعیت | تغییرات کلیدی |
|---|---|---|
| draft-04 | قدیمی | بسیاری کتابخانههای قدیمی همچنان پیشفرض آن را دارند |
| draft-07 | استاندارد عملی | گستردهترین پشتیبانی، غنیترین اکوسیستم |
| 2019-09 | گذار | $defs، unevaluatedProperties را معرفی کرد |
| 2020-12 | توصیهشده کنونی | items به prefixItems + items تقسیم شد، از $dynamicRef پشتیبانی میکند |
دو تله مهاجرت:
- نحو tuple تغییر کرد: در draft-07 آرایهمقدارِ
itemsیک tuple بیان میکرد؛ 2020-12 آن را بهprefixItemsبرد؛ $refو کلیدواژههای همسایه: در draft-07 وقتی$refحضور دارد کلیدواژههای همسایه نادیده گرفته میشوند؛ 2020-12 همزیستی آنها را مجاز میکند.
اگر اعتبارسنج فقط draft-07 را پشتیبانی کند، نوشتن نحو 2020-12 بیصدا نادیده گرفته میشود — و خطرناکترین حالت «اعتبارسنجی قبول میشود» است وقتی اصلاً چیزی اعتبارسنجی نشده باشد.
چگونه از Schema نوع TypeScript تولید کنم؟
دو مسیر: استخراج نوع از Schema، یا تولید Schema از نوع.
از Schema به نوع (بکاند قرارداد را تعریف میکند، فرانتاند مصرف میکند):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
نکته کلیدی as const است: بدون آن، type: 'string' بهجای literal 'string' بهصورت string استنتاج میشود و استنتاج میشکند.
از نوع به Schema (فرانتاند از قبل نوع دارد و اعتبارسنجی زمان اجرا میخواهد):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
مزیت این مسیر این است که «یک تعریف هم نوع و هم اعتبارسنجی زمان اجرا میدهد»؛ هزینهاش معرفی یک DSL است،
و Schema تولیدشده ممکن است جزئیات پیادهسازی را حمل کند (مثلاً ساختار $ref نازیبا).
وقتی اعتبارسنجی شکست میخورد چگونه فیلد مشخص را پیدا کنم؟
به instancePath (2020-12) یا dataPath (draft-07) که اعتبارسنج برمیگرداند نگاه کنید؛ یک JSON Pointer به مکان خطا است.
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price" مستقیم میگوید: فیلد price از عنصر سوم آرایه items نوع اشتباه دارد.
دو پیشنهاد عملی:
- همه خطاها را یکجا برگردانید نه اینکه روی اولی خطا بدهید — اصلاح همهچیز توسط فرانتاند در یک مرور سریعتر از رفتوبرگشت است؛
- Pointer را پیش از نمایش به کاربر نهایی به زبان ساده برگردانید؛
/items/2/priceبرای توسعهدهنده مفید و برای کاربر بیفایده است.
allErrors: true در بیشتر اعتبارسنجها کلید است (Ajv نیاز به روشن کردن صریح دارد)؛ پیشفرض اغلب حالت fail-fast است.
آیا additionalProperties: false باید فعال شود؟
برای قراردادهای داخلی فعال کنید؛ با APIهای عمومی احتیاط کنید.
پس از فعال شدن، هر فیلدی که زیر properties اعلام نشده باشد اعتبارسنجی را رد میکند. مزیت گرفتن تایپو
(userNmae) و عبور غیرمنتظره است؛ عیبش این است که افزودن فیلد توسط بالادست مصرفکنندههای پاییندست را میشکند و سازگاری رو به عقب را از بین میبرد.
مصالحه:
| سناریو | توصیه |
|---|---|
| بین سرویسهای داخلی | فعال کنید. تغییرات فیلد از مدیریت نسخه عبور میکند |
| APIهای عمومی بیرونی | غیرفعال کنید. اجازه دهید بالادست فیلد اضافه کند |
| مصرف داده شخص ثالث | غیرفعال کنید. طرف مقابل را کنترل نمیکنید |
| اعتبارسنجی پیکربندی | فعال کنید. تایپو در پیکربندی باید فوراً شکست بخورد |
چگونه یکتا بودن عناصر آرایه را اعتبارسنجی کنم؟
دو معناشناسی متفاوت — اشتباه نگیرید:
{ "uniqueItems": true }
این کل عنصر را یکتا میکند و میخواهد آرایه هیچ دو عنصر عمیقاً برابر نداشته باشد.
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
قطعه بالا فقط تضمین میکند خود شیءها بهعنوان یک کل تکراری نباشند؛ {"id":"a","name":"x"} و {"id":"a","name":"y"} تکراری حساب نمیشوند.
برای یکتا کردن بر اساس فیلد مشخص، Schema نمیتواند بیان کند؛ باید در لایه کسبوکار اعتبارسنجی کنید:
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id 重复');
}
آیا format واقعاً ایمیل و تاریخ را اعتبارسنجی میکند؟
لزوماً نه. در مشخصات، format بهصورت پیشفرض یک «حاشیهنویسی» است نه «تأیید»، پس اعتبارسنج ممکن است آن را جمع کند بدون آنکه بررسیاش کند.
- Ajv نیاز به نصب و پیکربندی بسته جداگانه
ajv-formatsدارد، وگرنهformat: "email"عملاً بیاثر است؛ - برخی اعتبارسنجها
formatرا تأییدیهای فعالشده بهصورت پیشفرض میگیرند، پس رفتار ناسازگار است؛ - حتی وقتی فعال است، regex ایمیل بسیار سهلگیر است —
a@bقبول میشود.
نتیجه: format را اعتبارسنجی قوی ندانید. اعتبارسنجی سختگیر ایمیل، URL و تاریخ را در کد کسبوکار انجام دهید؛
نگه داشتن format در Schema بهعنوان مستندات ارزشمند است، اما انتظار نداشته باشید داده کثیف را متوقف کند.
۳. مرجع سریع
| پرسش | نتیجه |
|---|---|
| یافتن گره بر اساس شرط | JSONPath |
| آدرسدهی دقیق، اعمال patch | JSON Pointer |
| تغییر عناصر آرایه | JSON Patch، Merge Patch نمیتواند |
| ادغام پیکربندی | Merge Patch |
| نسخه Schema برای پروژه جدید | 2020-12، به prefixItems توجه کنید |
| تولید نوع TS از Schema | json-schema-to-ts + as const |
| مکانیابی خطاهای اعتبارسنجی | instancePath را بخوانید، allErrors را فعال کنید |
| یکتا کردن بر اساس فیلد | Schema نمیتواند؛ از Set در لایه کسبوکار استفاده کنید |
format: email |
بیشتر اعتبارسنجها بهصورت پیشفرض بررسی نمیکنند، پس به آن تکیه نکنید |