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

جمعبندی: اول 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و~0escape میشود. پیش از نمایش برگردانید، وگرنه 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';
}
سه قاعده اینجا مهم است:
- جملهها را به هم نچسبانید. «Email format is invalid» و معادل چینیاش ترتیب کلمه یکسان ندارند و مونتاژ رشته در نهایت در یکی بیمعنا تولید میکند.
- جمع و واحد شمارش را به کتابخانه i18n بسپارید. هرگز «characters» را در قالب سختکد نکنید؛
1 characterو3 charactersمتفاوتاند. - برچسب فیلدها را هم ترجمه کنید.
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ها یا چیدمان فایل داخلی.