JSON Toolkit

כששגיאות JSON Schema חסרות משמעות: איתור שגיאות Ajv וממשל הודעות

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

מתקדםכ-10 דקות קריאהפורסם 2026-10-31עודכן 2026-10-31
תרשים איתור שגיאות אימות של Ajv וממשל הודעות

שורה תחתונה: קראו קודם את 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';
}

שלושה כללים חשובים כאן:

  1. אל תחברו משפטים. "Email format is invalid" והמקבילה בעברית לא חולקות סדר מילים, והרכבת מחרוזות תפיק בסוף שטויות באחת מהן.
  2. הפקידו רבים ומילות מידה לספריית ה-i18n. לעולם אל תקודדו "characters" בתבנית; 1 character ו-3 characters שונים.
  3. תרגמו גם תוויות שדות. 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 פנימיים, ביטויים רגולריים או פריסת קבצים.