JSON Toolkit

وقتی خطاهای JSON Schema بی‌معنا می‌شوند: مکان‌یابی خطا و مدیریت پیام در Ajv

جمع‌بندی: اعتبارسنج خطاهای ماشینی می‌دهد؛ کاربران می‌خواهند بدانند کدام جعبه ورودی اشتباه است. آرایه errors در Ajv، instancePath در برابر schemaPath، گروه‌بندی و جمله‌بندی را پوشش می‌دهد.

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

جمع‌بندی: اول instancePath را بخوانید تا داده شکست‌خورده را مکان‌یابی کنید، سپس schemaPath را تا قاعده‌ای که فعال شد؛ برای required و additionalProperties، نام کلید واقعی را از params بیرون بکشید پیش از انتخاب پیام.

کار اعتبارسنج این است که بگوید کدام قاعده شکست خورد. ترجمه آن به «این جعبه ورودی اشتباه است» کار شماست.

۱. آرایه errors واقعاً چه چیزی می‌دهد

Ajv روی شکست اعتبارسنجی خطا نمی‌اندازد؛ نتیجه را در validate.errors می‌گذارد. پیکربندی پیش‌فرض فقط نخستین خطا را نگه می‌دارد و allErrors همان چیزی است که فهرست کامل می‌دهد. یک شیء خطای واحد پنج فیلد مهم دارد و فهمشان پیش‌شرط هر چیز پایین‌دستی است:

فیلد معنا مثال
instancePath JSON Pointer به مقدار شکست‌خورده /email
schemaPath مکان کلیدواژه شکست‌خورده در schema #/properties/email/format
keyword نام قید نقض‌شده format
params پارامترهای ماشین‌خوان آن قید { "format": "email" }
message نثر انگلیسی که بین نسخه‌ها تغییر می‌کند must match format "email"

یک خطای واقعی این‌گونه است:

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

توجه کنید message نثری برای انسان است، نه رابط پایدار. Ajv ممکن است بین نسخه‌ها بازنویسی‌اش کند و نمی‌تواند به محصول بومی‌سازی‌شده خدمت کند. هرگز با عبارت باقاعده تجزیه‌اش نکنید و هرگز عیناً به کلاینت برنگردانید.

۲. instancePath و schemaPath به پرسش‌های متفاوت پاسخ می‌دهند

instancePath می‌گوید کدام برش داده شکست خورد؛ schemaPath می‌گوید کدام قاعده فعال شد. دو دستگاه مختصات‌اند و متقارن نیستند:

  • instancePath یک JSON Pointer مطابق RFC 6901 است. رشته خالی یعنی ریشه؛ /items/0/price یعنی فیلد price نخستین عنصر آرایه.
  • schemaPath با # شروع می‌شود و به کلیدواژه داخل سند schema اشاره می‌کند. عبور از یک $ref، به داخل schema ارجاع‌شده اشاره می‌کند، پس به $id نیاز دارید تا بدانید از کدام فایل آمده.
  • کلیدی شامل / یا ~ به‌ترتیب به ~1 و ~0 escape می‌شود. پیش از نمایش برگردانید، وگرنه UI نمادهای گیج‌کننده نشان می‌دهد.

مثلاً instancePath: "/email" به‌همراه schemaPath: "#/properties/email/format" یعنی فیلد email روی شیء ریشه در بررسی format پاس نشد.

رایج‌ترین تله required و additionalProperties است: instancePathشان به شیء والد اشاره می‌کند، نه به کلید خطادار.

کلیدواژه instancePath به چه اشاره می‌کند کلید واقعی کجاست
required شیء والد (رشته خالی در ریشه) params.missingProperty
additionalProperties شیء والد params.additionalProperty
type خود مقدار شکست‌خورده —
enum خود مقدار شکست‌خورده params.allowedValues

هنگام نگاشت خطاها به فیلدهای فرم، اول این دو حالت را مدیریت کنید. وگرنه «email گم شده» به «خطای شیء ریشه» تبدیل می‌شود و کاربر نمی‌فهمد کجا تایپ کند.

۳. allErrors و verbose: کامل بودن در برابر هزینه

پیش‌فرض allErrors: false باعث می‌شود Ajv روی نخستین شکست بایستد. برای ابزار خط فرمان خوب و برای فرم فاجعه است: کاربر یک فیلد را درست می‌کند، می‌فرستد، خطای بعدی را می‌بیند و سه بار تکرار می‌کند تا فرم پاس شود.

تنظیم رفتار مزیت هزینه
allErrors: false فقط نخستین خطا سریع‌ترین، کمترین حافظه ارسال مکرر
allErrors: true جمع همه خطاها همه مشکلات در یک مرور تعداد خطا در schemaهای پیچیده منفجر می‌شود
verbose: true ضمیمه schema و data به هر خطا تشخیص سمت سرور آسان‌تر اندازه به‌شدت بالا؛ هرگز افشا نکنید
messages: false حذف فیلد message payload کوچک‌تر از دست دادن سرنخ انگلیسی عیب‌یابی

تقسیم پیشنهادی: برای اعتبارسنجی سرور allErrors: true فعال و آرایه کامل در لاگ‌های داخلی نگه دارید، در حالی که پاسخ عمومی فقط فیلدهای فهرست سفید را حمل می‌کند. verbose را فقط هنگام بررسی فعال کنید؛ قطعه schema تطبیق‌یافته و داده کاربر را در هر خطا جاسازی می‌کند، که حافظه هزینه دارد و می‌تواند مقادیر حساس را به لاگ نشت دهد.

حجم خطا مشکل رایج دیگر است. شاخه anyOf شکست‌خورده چند خطای فرزند به‌علاوه یک خطای خلاصه تولید می‌کند و oneOf با شاخه‌های زیاد بدتر است. آرایه را عیناً نشان دهید و کاربران برای یک فیلد پنج پیام تقریباً تکراری می‌بینند. گروه‌بندی باید پیش از رندر رخ دهد.

۴. خطاها را بر اساس فیلد گروه‌بندی کنید

گروه‌بندی سه کار می‌کند: مسیر را به نام فیلد برگرداند، آفست required و additionalProperties را تصحیح کند و چند خطا روی یک فیلد را در یک پیام عملی‌ترین ادغام کند.

/** One validation error ready for display */
interface FieldError {
  field: string;
  keyword: string;
  params: Record<string, unknown>;
}

/**
 * Group Ajv errors by field.
 * @param errors Error array produced by Ajv
 * @returns Map keyed by field path, each value listing its errors
 */
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
  const grouped: Record<string, FieldError[]> = {};

  for (const errorItem of errors) {
    const keyword: string = errorItem.keyword;
    let field: string = errorItem.instancePath;

    if (keyword === 'required') {
      const parentPath: string = errorItem.instancePath;
      field = `${parentPath}/${errorItem.params.missingProperty}`;
    } else if (keyword === 'additionalProperties') {
      field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
    }

    const normalizedField = field === '' ? '(root)' : field;
    (grouped[normalizedField] ??= []).push({
      field: normalizedField,
      keyword,
      params: errorItem.params ?? {},
    });
  }

  return grouped;
}

با ترتیب اولویت ادغام کنید: اول required (گم‌شده)، سپس type، سپس enum/const، سپس format/pattern، سپس طول و بازه. برای هر فیلد فقط پیام بالاترین اولویت را نشان دهید و بقیه را در لاگ نگه دارید. دلیل سرراست است: وقتی نوع اشتباه است، پیام طول نویز است و اصلاح نوع تنها گام معقول بعدی است.

خطاهای خلاصه oneOf و anyOf معمولاً اطلاعات عملی ندارند. هنگام رندر ردشان کنید و فقط خطاهای شاخه‌ای را که به فیلد مشخص اشاره می‌کنند نگه دارید.

۵. پیام‌های سفارشی: مسیر افزونه و مسیر قالب

Ajv جمله‌بندی بومی‌شده ندارد، پس دو گزینه عملی هست.

گزینه یک: افزونه ajv-errors. کلیدواژه errorMessage اضافه می‌کند تا پیام‌ها کنار قیدها زندگی کنند:

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

مزیتش این است که جمله‌بندی با فیلد سفر می‌کند و schema هم‌زمان نقش مستندات را بازی می‌کند. عیبش این است که متن در یک زبان قفل می‌شود، پس بومی‌سازی یعنی نگهداری چند schema. افزونه همچنین فقط وقتی هر پیام را جایگزین می‌کند که allErrors: true تنظیم شده باشد؛ آن کلید را فراموش کنید و ترکیبی از سفارشی و انگلیسی پیش‌فرض می‌گیرید.

گزینه دو: جدول کلیدواژه-به-قالب. اعتبارسنج فقط keyword و params می‌دهد؛ برنامه جمله‌بندی را تولید می‌کند:

کلیدواژه قالب پارامترهای موجود
required فیلد الزامی گم‌شده: {field} missingProperty
type نوع مورد انتظار: {type} type
minLength دست‌کم {limit} نویسه وارد کنید limit
maximum نباید از {limit} بیشتر شود limit
format قالب نامعتبر: {format} format
enum باید یکی از گزینه‌های مجاز باشد allowedValues
pattern مقدار با قالب مورد انتظار نمی‌خواند pattern (بازتابش ندهید)

گزینه دو برای محصولاتی مناسب است که به چند زبان و صدای یکدست نیاز دارند. اعتبارسنجی را از ارائه جدا می‌کند، ترجمه را از schema بیرون نگه می‌دارد و به فرانت‌اند اجازه می‌دهد همان قالب‌ها را بازاستفاده کند.

۶. بومی‌سازی: پیام‌ها به اعتبارسنج تعلق ندارند

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

/** Validation message templates keyed by Ajv keyword */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
  required: (params) => `Missing required field: ${params.missingProperty}`,
  type: (params) => `Expected type: ${params.type}`,
  minLength: (params) => `Enter at least ${params.limit} characters`,
  maximum: (params) => `Must not exceed ${params.limit}`,
  format: () => 'Invalid format',
};

/**
 * Build one user-facing validation message.
 * @param keyword Name of the failed constraint
 * @param params Constraint parameters
 * @returns Localized message text
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

سه قاعده اینجا مهم است:

  1. جمله‌ها را به هم نچسبانید. «Email format is invalid» و معادل چینی‌اش ترتیب کلمه یکسان ندارند و مونتاژ رشته در نهایت در یکی بی‌معنا تولید می‌کند.
  2. جمع و واحد شمارش را به کتابخانه i18n بسپارید. هرگز «characters» را در قالب سخت‌کد نکنید؛ 1 character و 3 characters متفاوت‌اند.
  3. برچسب فیلدها را هم ترجمه کنید. params.missingProperty برابر email است، در حالی که UI باید «Email address» بگوید. آن نگاشت را در پیکربندی فرم نگه دارید، نه در بسته زبان.

۷. ساختار داخلی را به کلاینت برنگردانید

برگرداندن schemaPath، params و message با هم در توسعه راحت و در تولید نشت اطلاعات است. allowedValues enumهای داخلی را افشا می‌کند، pattern regexهای اعتبارسنجی را، و schemaPath چیدمان فایل و نام‌گذاری فیلد را — در برخی موارد نقشه نقاط ضعف شما را به مهاجم می‌دهد.

پاسخ عمومی دقیقاً سه چیز نگه می‌دارد: مسیر فیلد (یا نام فیلد فرم)، کد خطای پایدار و متن کاربر-facing. به‌جای keyword خام از کدهای کوتاه پایدار استفاده کنید تا بعداً جا برای تغییر اعتبارسنج داشته باشید.

{
  "type": "about:blank",
  "title": "Request validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
    { "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
  ]
}

لاگ‌های سرور باید عکسش را نگه دارند: آرایه کامل خطا، شناسه درخواست و نسخه schema، تا بررسی بتواند پاسخ دهد «کدام قاعده این را مسدود کرد». از ریختن کل بدنه درخواست در لاگ پرهیز کنید؛ مسیرها و کلیدواژه‌های شکست‌خورده را نگه دارید.

۸. ارائه در فرانت‌اند: از هایلایت فیلد تا نشانگرهای ویرایشگر

خطاها در نهایت به UI می‌رسند و ارائه به بافت بستگی دارد:

بافت ارائه اقدام کلیدی
فرم متن قرمز زیر فیلد، فوکوس روی نخستین خطا نگاشت مسیرها به نام فرم؛ نگه داشتن اندیس آرایه
ورود انبوه جدول شماره ردیف، نام ستون، خلاصه، قابل خروجی تبدیل /items/3/price به ردیف ۴، ستون price
ویرایشگر JSON نشانگرهای درون‌خطی و پنل مشکلات استفاده از پارسر برای نگاشت JSON Pointer به خط و ستون
ابزار عیب‌یابی API فهرست خطای ساختاریافته با نگه داشتن schemaPath توسعه‌دهنده-facing است، پس جزئیات بیشتر قابل قبول

سه نکته پیاده‌سازی:

  • برای هر فیلد فقط نخستین خطا را نشان دهید، بقیه را در «۲ مشکل دیگر» جمع کنید، تا فرم در قرمز غرق نشود.
  • اندیس آرایه را برای هدف‌گیری نگه دارید اما در برچسب‌ها حذفش کنید. «ردیف ۴، price» بهتر از /items/3/price خوانده می‌شود.
  • سناریوهای ویرایشگر به نگاشت موقعیت نیاز دارند. جست‌وجوی رشته ساده وقتی نام فیلد تکرار می‌شود غیرقابل اعتماد است؛ پارسر باید offset هر گره را بدهد که بعد به خط و ستون تبدیل می‌کنید.

اگر JSON از تایپ کاربر می‌آید نه فرم، اول تأیید کنید سند از نظر نحوی معتبر است. خطاهای نحوی هنگام تجزیه رخ می‌دهند و هرگز به اعتبارسنجی schema نمی‌رسند. تجزیه و قالب‌بندی JSON خط و ستون دقیق را گزارش می‌کند، که از دنبال کردن خطاهای schema بسیار سریع‌تر است. برای اینکه schemaهای بیش از حد سخت‌گیر هنگام استنتاج از نمونه‌ها چگونه پیدا می‌شوند، چرا JSON Schema تولیدشده خودکار داده واقعی را رد می‌کند را ببینید؛ برای پیکربندی اعتبارسنج و سیستم کلیدواژه، JSON Schema: از مبانی تا تولید.

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

چرا فقط یک خطا می‌گیرم؟

allErrors به‌صورت پیش‌فرض false است، پس Ajv روی نخستین شکست برمی‌گردد. برای اعتبارسنجی فرم صریحاً روشنش کنید؛ وگرنه کاربران باید مکرر بفرستند تا هر مشکل را کشف کنند.

چرا مسیر روی خطای required خالی است؟

instancePath برای required به شیء والد اشاره می‌کند که در ریشه رشته خالی است؛ نام کلید گم‌شده در params.missingProperty است. دو را به هم بچسبانید تا مسیر کامل شود. نمایش instancePath تنها «خطای شیء ریشه» بی‌فایده را می‌دهد.

آیا می‌توانم message را مستقیم به کاربران نشان دهم؟

توصیه نمی‌شود. نثر انگلیسی است که جمله‌بندی‌اش بین نسخه‌ها تغییر می‌کند و قابلیت بومی‌سازی ندارد. به‌جایش متن را از keyword و params تولید کنید و message را برای لاگ نگه دارید.

چرا پیام خطای سفارشی‌ام نادیده گرفته می‌شود؟

اول بررسی کنید allErrors: true فعال است؛ ajv-errors فقط با آن کلید می‌تواند همه پیام‌ها را جایگزین کند. سپس تأیید کنید errorMessage هم‌سطح کلیدواژه شکست‌خورده است — یک سطح اختلاف و شناخته نمی‌شود.

کدام فیلدها در تولید امن‌اند برگردانده شوند؟

مسیر فیلد، کد خطای پایدار و متن کاربر-facing. schemaPath، params و message را در لاگ سرور نگه دارید: این عیب‌یابی را پشتیبانی می‌کند بدون افشای enumها، regexها یا چیدمان فایل داخلی.