כששגיאות JSON Schema חסרות משמעות: איתור שגיאות Ajv וממשל הודעות
שורה תחתונה: המאמת מפיק שגיאות מכונה; המשתמשים רוצים לדעת איזו תיבת קלט שגויה. המאמר מכסה את מערך errors של Ajv, instancePath מול schemaPath, קיבוץ וניסוח.

שורה תחתונה: קראו קודם את instancePath כדי לאתר את הנתונים שנכשלו, ואז את schemaPath כדי לאתר את הכלל שהופעל; עבור required ו-additionalProperties, חלצו את שם המפתח האמיתי מתוך params לפני שבוחרים את ההודעה.
תפקידו של המאמת הוא לומר איזה כלל נכשל. לתרגם את זה ל"תיבת הקלט הזו שגויה" הוא התפקיד שלכם.
1. מה מערך 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 עשוי לנסח אותו מחדש בין גרסאות, והוא לא יכול לשרת מוצר מתורגם. אל תפרסרו אותו בביטוי רגולרי, ואל תחזירו אותו כמו שהוא ללקוח.
2. instancePath ו-schemaPath עונים על שאלות שונות
instancePath אומר איזה חלק של הנתונים נכשל; schemaPath אומר איזה כלל הופעל. אלה שתי מערכות קואורדינטות, והן לא סימטריות:
instancePathהוא JSON Pointer לפי RFC 6901. מחרוזת ריקה פירושה השורש;/items/0/priceפירושו שדה המחיר של האיבר הראשון במערך.schemaPathמתחיל ב-#ומצביע על מילת המפתח בתוך מסמך ה-schema. כשהוא חוצה$ref, הוא מצביע בתוך ה-schema המופנה, ולכן צריך את ה-$idכדי לדעת מאיזה קובץ הוא הגיע.- מפתח שמכיל
/או~עובר escaping ל-~1ול-~0בהתאמה. בטלו זאת לפני התצוגה, אחרת הממשק יציג סימנים מבלבלים.
לדוגמה, instancePath: "/email" יחד עם schemaPath: "#/properties/email/format" פירושו ששדה האימייל באובייקט השורש לא עבר את בדיקת ה-format.
המלכודת הנפוצה ביותר היא required ו-additionalProperties: ה-instancePath שלהם מצביע על אובייקט האב, ולא על המפתח הפוגע.
| מילת מפתח | ה-instancePath מצביע על |
איפה המפתח האמיתי נמצא |
|---|---|---|
required |
אובייקט האב (מחרוזת ריקה בשורש) | params.missingProperty |
additionalProperties |
אובייקט האב | params.additionalProperty |
type |
הערך שנכשל עצמו | — |
enum |
הערך שנכשל עצמו | params.allowedValues |
כשממפים שגיאות לשדות טופס, טפלו קודם בשני המקרים האלה. אחרת "האימייל חסר" הופך ל"שגיאה באובייקט השורש", ולמשתמש אין מושג איפה להקליד.
3. allErrors ו-verbose: שלמות מול עלות
ברירת המחדל allErrors: false גורמת ל-Ajv לעצור בכשל הראשון. זה בסדר לכלי שורת פקודה והרסני לטופס: המשתמש מתקן שדה אחד, שולח, רואה את השגיאה הבאה, וחוזר שלוש פעמים לפני שהטופס עובר.
| הגדרה | התנהגות | תועלת | עלות |
|---|---|---|---|
allErrors: false |
רק השגיאה הראשונה | הכי מהיר, הכי פחות זיכרון | שליחות חוזרות |
allErrors: true |
אוסף כל שגיאה | כל הבעיות במעבר אחד | מספר השגיאות מתפוצץ ב-schema מורכב |
verbose: true |
מצרף schema ו-data לכל שגיאה |
אבחון קל יותר בצד השרת | הגדלה משמעותית של הגודל; לעולם לא לחשוף אותו |
messages: false |
משמיט את שדה ה-message |
מטענים קטנים יותר | מאבד את רמז הדיבוג באנגלית |
פיצול מומלץ: הפעילו allErrors: true לאימות בצד השרת ושמרו את המערך המלא בלוגים פנימיים, בעוד שהתגובה הציבורית נושאת רק שדות ברשימת היתר. הפעילו verbose רק בזמן חקירה; הוא מטמיע בכל שגיאה את קטע ה-schema שהתאים ואת נתוני המשתמש, מה שעולה זיכרון ועלול להדליף ערכים רגישים ללוגים.
נפח השגיאות הוא הבעיה הנפוצה הנוספת. ענף anyOf שנכשל מפיק כמה שגיאות ילדים ועוד שגיאת סיכום אחת, ו-oneOf עם ענפים רבים גרוע יותר. הציגו את המערך כמו שהוא והמשתמשים יראו חמש הודעות כמעט זהות עבור שדה אחד. הקיבוץ חייב לקרות לפני הרינדור.
4. קבצו שגיאות לפי שדה
קיבוץ עושה שלושה דברים: מחזיר את הנתיב לשם שדה, מתקן את ההיסט של 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 בדרך כלל לא נושאות מידע יעיל. דלגו עליהן ברינדור והשאירו רק את שגיאות הענפים שמצביעות על שדה קונקרטי.
5. הודעות מותאמות: מסלול התוסף ומסלול התבנית
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 משמש גם כתיעוד. החסרון הוא שהטקסט קפוא בשפה אחת, ולכן לוקליזציה פירושה תחזוקת כמה schemas. התוסף גם מחליף את כל ההודעות רק כש-allErrors: true מוגדר; אם שוכחים את הדגל הזה מקבלים תערובת של הודעות מותאמות ואנגלית ברירת מחדל.
אפשרות שנייה: טבלת מילת מפתח לתבנית. המאמת מספק רק keyword ו-params; האפליקציה מייצרת את הניסוח:
| מילת מפתח | תבנית | פרמטרים זמינים |
|---|---|---|
required |
חסר שדה חובה: {field} | missingProperty |
type |
צפוי טיפוס: {type} | type |
minLength |
הזינו לפחות {limit} תווים | limit |
maximum |
אסור לחרוג מ-{limit} | limit |
format |
פורמט לא תקין: {format} | format |
enum |
חייב להיות אחד מהאפשרויות המותרות | allowedValues |
pattern |
הערך אינו תואם לפורמט הצפוי | pattern (אל תחזירו אותו) |
אפשרות שנייה מתאימה למוצרים שצריכים כמה שפות וקול אחיד. היא מפרידה בין אימות לתצוגה, משאירה את התרגום מחוץ ל-schema, ומאפשרת לפרונט להשתמש באותן תבניות.
6. לוקליזציה: הודעות לא שייכות למאמת
העברת הניסוח לחבילות שפה היא נקודת הפתיחה של ממשל ההודעות. חבילת שפה שומרת תבניות ומפתחות, ולעולם לא שמות שדות; תוויות שדות מגיעות מתצורת הטופס או מחוזה ה-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, בעוד שהממשק צריך להציג "כתובת אימייל". שמרו את המיפוי בתצורת הטופס, לא בחבילת השפה.
7. אל תחזירו מבנה פנימי ללקוחות
החזרת schemaPath, params ו-message יחד נוחה בזמן הפיתוח והדלפת מידע בפרודקשן. allowedValues חושף enums פנימיים, pattern חושף ביטויים רגולריים של אימות, ו-schemaPath חושף את פריסת הקבצים ואת שמות השדות — במקרים מסוימים זה מוסר לתוקפים מפה של נקודות התורפה שלכם.
התגובה הציבורית שומרת בדיוק שלושה דברים: נתיב שדה (או שם שדה טופס), קוד שגיאה יציב וטקסט למשתמש. השתמשו בקודים קצרים ויציבים במקום ב-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, כך שחקירה תוכל לענות "איזה כלל חסם את זה". הימנעו מהשלכת גופי בקשה שלמים ללוגים; שמרו במקום את הנתיבים ומילות המפתח שנכשלו.
8. תצוגה בפרונט: מהדגשת שדה ועד סימונים בעורך
השגיאות בסוף נוחתות בממשק, והתצוגה תלויה בהקשר:
| הקשר | תצוגה | פעולה מרכזית |
|---|---|---|
| טופס | טקסט אדום מתחת לשדה, מיקוד בשגיאה הראשונה | מיפוי נתיבים לשמות טופס; שמירה על אינדקסים במערך |
| ייבוא טבלה בכמות | מספר שורה, שם עמודה, סיכום, ניתן לייצוא | הפיכת /items/3/price לשורה 4, עמודת מחיר |
| עורך JSON | סימונים בתוך השורות ופאנל בעיות | שימוש בפרסר כדי למפות JSON Pointer לשורה ועמודה |
| כלי דיבוג API | רשימת שגיאות מובנית ששומרת על schemaPath |
מיועד למפתחים, ולכן יותר פירוט מקובל |
שלוש הערות מימוש:
- הציגו רק את השגיאה הראשונה לכל שדה, וכַווצו את השאר ל"עוד 2 בעיות", כדי שהטופס לא יטובע באדום.
- שמרו אינדקסים של מערכים לצורך איתור אבל השמיטו אותם בתוויות. "שורה 4, מחיר" נקרא טוב יותר מ-
/items/3/price. - בתרחישי עורך צריך מיפוי מיקום. חיפוש מחרוזת פשוט אינו אמין כששם שדה חוזר; הפרסר צריך לספק היסטים לכל צומת, שאותם ממירים לשורה ועמודה.
אם ה-JSON מגיע ממשתמש שמקליד ולא מטופס, אשרו קודם שהמסמך תקין תחבירית. שגיאות תחביר מתרחשות בפרסור ולעולם לא מגיעות לאימות schema. JSON Parse & Format מדווח על השורה והעמודה המדויקות, שזה הרבה יותר מהיר מרדיפה אחרי שגיאות schema. לאיך נוצרים schemas נוקשים מדי כשמסיקים אותם מדוגמאות, ראו למה JSON Schema שנוצר אוטומטית ממשיך לדחות נתונים אמיתיים; לתצורת המאמת ולמערכת מילות המפתח, ראו JSON Schema: מהיסודות לפרודקשן.
9. שאלות נפוצות
למה אני מקבל רק שגיאה אחת?
allErrors הוא false כברירת מחדל, ולכן Ajv מחזיר בכשל הראשון. הפעילו אותו במפורש לאימות טפסים; אחרת המשתמשים חייבים לשלוח שוב ושוב כדי לגלות כל בעיה.
למה הנתיב בשגיאת required ריק?
ה-instancePath של required מצביע על אובייקט האב, שהוא המחרוזת הריקה בשורש; שם המפתח החסר נמצא ב-params.missingProperty. חברו את השניים כדי לקבל את הנתיב המלא. הצגת instancePath לבדו מניבה את "שגיאה באובייקט השורש" חסרת התועלת.
אפשר להציג message ישירות למשתמשים?
לא מומלץ. זו פרוזה באנגלית שהניסוח שלה משתנה בין גרסאות, ואין לה יכולת לוקליזציה. צרו טקסט מתוך keyword ו-params ושמרו את message ללוגים.
למה הודעת השגיאה המותאמת שלי מתעלמת?
בדקו קודם ש-allErrors: true מופעל; ajv-errors יכול להחליף את כל ההודעות רק עם הדגל הזה. אחר כך אשרו ש-errorMessage יושב באותה רמה כמו מילת המפתח שנכשלה — רמה אחת לא נכונה והוא לא יזוהה.
אילו שדות בטוחים להחזיר בפרודקשן?
נתיב שדה, קוד שגיאה יציב וטקסט למשתמש. שמרו schemaPath, params ו-message בלוגי השרת: זה תומך בדיבוג בלי לחשוף enums פנימיים, ביטויים רגולריים או פריסת קבצים.