JSON Toolkit

JSON Schema: מהיסודות לפרודקשן

שורה תחתונה: יש הרבה מילות מפתח, אבל רק תריסר משמשות ביום-יום. תשיגו נכון את type/properties/required/$ref, תטפלו היטב בהודעות שגיאה, וכיסיתם שמונים אחוז מהמקרים האמיתיים.

בינוניכ-14 דקות קריאהפורסם 2026-10-30עודכן 2026-10-30
עטיפה עבור JSON Schema: מהיסודות לפרודקשן

שורה תחתונה: נעלו קודם את $schema ל-2020-12, ואז חלצו מבנים משותפים עם $defs + $ref; השתמשו ב-oneOf רק כשבדיוק ענף אחד חייב להתאים, והעדיפו discriminators או anyOf בכל מקום אחר.

schema אינו נספח לתיעוד; הוא נכס שניתן להריץ, לבדוק ולייצר ממנו.

1. קודם כל סדרו את הגרסה

גרסה ערך $schema סטטוס הערות
draft-04 http://json-schema.org/draft-04/schema# מיושן פרויקטי legacy וחלק מכלי OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# בשימוש נרחב הבסיס דה-פקטו של OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema זמין הציג את $defs ואת אוצר המילים של ההערות
2020-12 https://json-schema.org/draft/2020-12/schema מומלץ כיום הציג את prefixItems, שינה את הסמנטיקה של items

השתמשו ב-2020-12 בפרויקטים חדשים. שימו לב שב-2020-12 items מגביל רק את "האיברים הנותרים" — הצהרות בסגנון tuple עברו ל-prefixItems. זו המלכודת הנפוצה ביותר בהגירה מ-draft-07.

2. schema מינימלי שמיש

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

ה-schema הזה מכסה את ארבעת הנושאים הנפוצים ביותר: אילוצי טיפוס, שדות חובה, מגבלות טווח ואילוצי איברי מערך.

3. מפת מילות מפתח

3.1 כלליות

מילת מפתח מטרה למה לשים לב
type הטיפוס; יכול להיות מערך "type": ["string", "null"] פירושו nullable
enum ערכים מנויים הערכים חייבים להיות ייחודיים
const אילוץ של ערך יחיד טוב לערכים קבועים כמו מספר גרסה
$ref מפנה ל-schema אחר מפנה ל-schema שלם ולא מתמזג עם מילות מפתח אחים באובייקט המפנה

3.2 מספרים

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. שימו לב שב-draft-04 ‏exclusiveMaximum היה בוליאני (בשילוב עם maximum); רק מ-draft-07 ואילך הוא הפך למילת מפתח מספרית עצמאית — מה שגורם ל-schemas רבים "שהועתקו מתיעוד ישן" להיכשל במצב strict.

3.3 מחרוזות

minLength, maxLength, pattern (ביטוי רגולרי ECMA-262) ו-format (הערה).

minLength סופר נקודות קוד Unicode, לא יחידות UTF-16 ולא בייטים. אימות עם string.length של JavaScript לא יסכים איתו.

3.4 מערכים

מילת מפתח מטרה
prefixItems אילוץ בסגנון tuple של 2020-12, מתאים איברים אחד אחד לפי אינדקס
items אילוץ אחיד על האיברים הנותרים (2020-12)
contains + minContains/maxContains לפחות/לכל היותר כמה איברים מתאימים חייבים להיות
uniqueItems האיברים חייבים כולם להיות שונים (השוואה עמוקה; הימנעו במערכים גדולים)
minItems / maxItems אילוצי אורך

3.5 אובייקטים

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

שלוש תפיסות שגויות נפוצות:

  1. required דורש רק שהמפתח יקיים: {"name": ""} תקין. כדי לדחות מחרוזת ריקה חייבים להוסיף minLength: 1;
  2. additionalProperties: false מגדיל בעיות תאימות: שדה מיותר אחד מלקוח מכשיל את כל המטען, כך שאבולוציה של ה-API כופה על כל הקוראים לשדרג בתיאום;
  3. properties לא מגביל מפתחות שלא הוכרזו — כברירת מחדל הם מותרים.

3.6 הרכבה לוגית

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
מילת מפתח סמנטיקה כשל טיפוסי
allOf הכל חייב להתקיים כמה ענפים מטילים אילוצים סותרים על אותו שדה
anyOf לפחות אחד חייב להתקיים קשה לאתר את הודעת השגיאה לענף מסוים
oneOf בדיוק אחד חייב להתקיים הנתונים מתאימים לשני ענפים ונכשלים, למרות שזה "נראה בבירור נכון"
not אסור שיתקיים משמש לעיתים רחוקות, והודעות השגיאה הכי פחות ידידותיות

oneOf הוא מילת המפתח הנוטה ביותר להפיק "נתונים תקינים שנכשלים באימות". אם אילוצי הענפים חופפים (למשל, שניהם מתירים שדות מיותרים), רשומה אחת מתאימה לכמה ענפים ו-oneOf נכשל לחלוטין. במקרה כזה, עברו ל-anyOf, או השתמשו בשדה discriminator עם const בתוך הענפים.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

סגנון ה-discriminator קריא יותר מ-oneOf, ומפיק שגיאות טובות יותר.

4. פיצול ושימוש חוזר

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs הוא הצורה שהוצגה ב-2019-09; draft-07 משתמש ב-definitions;
  • הפניות בין קבצים משתמשות ב-$id יחסיים, אבל אל תסתמכו על שליפת schemas מרוחקים בזמן ריצה בפרודקשן — ארזו אותם כקבצים סטטיים במקום;
  • אי אפשר להוסיף אילוצים לצד הפניה (התנהגות $ref עם מילות מפתח אחים משתנה לפי גרסה). כשצריך הרכבה, השתמשו ב-allOf.

5. אימות בזמן ריצה עם Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

נקודות מרכזיות:

  • קראו ל-compile רק פעם אחת, ברמת מודול עליון; קומפילציה מחדש בכל בקשה הופכת אימות של 10 ms למאות מילישניות;
  • strict: true חושף מילות מפתח עם שגיאת הקלדה מוקדם (למשל minlength במקום minLength), ולכן שווה להפעיל;
  • רק ajv-formats גורם ל-format: "email" ול-format: "date-time" להיכנס לתוקף בפועל;
  • 2020-12 דורש את נקודת הכניסה ajv/dist/2020 — אל תשתמשו בייצוא ברירת המחדל.

6. איך לעצב הודעות שגיאה

מה שהמאמת נותן לכם הוא "איזו מילת מפתח נכשלה"; מה שהמשתמש רוצה הוא "איזו תיבת קלט שגויה". שווה להוסיף שכבת מיפוי:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

בצד ה-API, שמרו מבנה קריא למכונה כדי שהפרונט יוכל להחליט איך להציג אותו:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. ביצועים ומלכודות

  • עלות האימות גדלה עם מורכבות ה-schema וגודל הנתונים; כמה ענפי oneOf גורמים לפיצוץ קומבינטורי, שבולט מאוד עם אלפי רשומות;
  • uniqueItems מבצע השוואה עמוקה ב-O(n²) על המערך; במערכים גדולים, הסירו כפילויות בשכבת העסקים במקום;
  • הימנעו מדפוסי נסיגה קטסטרופלית ב-pattern (כמו (a+)+$); זו נקודת הכניסה הקלאסית ל-ReDoS;
  • כשמשלבים additionalProperties: false עם allOf, שדות שהוכרזו ב-sub-schema נשפטים כ"מיותרים" על ידי האב, ולכן צריך למזג אותם ל-schema אחד או להשתמש ב-unevaluatedProperties.

8. רשימת בדיקה

  • [ ] נעלו 2020-12 והצהירו על $schema בראש הקובץ;
  • [ ] חלצו מבנים משותפים עם $defs + $ref במקום להעתיק-להדביק;
  • [ ] העדיפו discriminators על פני oneOf;
  • [ ] השתמשו שוב בתוצאת compile של המאמת והפעילו מצב strict;
  • [ ] תעדו בבירור האם format נאכף;
  • [ ] תגובות שגיאה מספקות את הנתיב, מילת המפתח והודעה למשתמש;
  • [ ] שימו את ה-schema ב-CI: הריצו את מקרי JSON Schema Test Suite כמבחני רגרסיה.

שאלות נפוצות

איך כדאי לבחור draft של JSON Schema?

השתמשו ב-2020-12 בפרויקטים חדשים והצהירו על $schema בראש הקובץ. אם ה-schema נכנס ל-OpenAPI 3.1, הוא חייב להיות גם 2020-12, כי 3.1 מטמיע את הגרסה הזו ישירות; השאירו draft-07 רק לפרויקטים שעדיין תלויים בכלי שרשרת ישנים. השניים מתנהגים אחרת לגבי items ולגבי מילות מפתח אחים ל-$ref, ולכן אל תערבבו אותם במערך schema אחד.

למה additionalProperties: false מגדיל בעיות תאימות?

הוא מתייחס לכל מפתח שלא הוכרז כשגיאה, כך ששדה מיותר אחד מלקוח מכשיל את כל המטען. כל קורא חייב אז לשדרג בתיאום בכל פעם שה-API מתפתח, אחרת לקוחות ישנים נדחים. schemas בסגנון תצורה צריכים לסגור שדות מיותרים במפורש, בעוד ש-APIs ציבוריים צריכים לשקול את מחיר התאימות.

כדאי להשתמש ב-oneOf או ב-anyOf?

השתמשו ב-oneOf רק כשהנתונים חייבים להתאים בדיוק לענף אחד. אם אילוצי הענפים יכולים להתקיים בו-זמנית, רשומה אחת מתאימה לכמה ענפים ו-oneOf נכשל לחלוטין; עברו ל-anyOf, או הוסיפו discriminator מסוג const בכל ענף.

מקורות

מאמרים קשורים

סנכרון דו-כיווני בין Schema לטיפוסי TypeScript

טיפוסים ו-schemas מכסים כל אחד מקטע אחר: אחד בזמן קומפילציה, אחד בזמן ריצה. השאלה אינה מה לבחור, אלא איך לשמור את השניים מסונכרנים אוטומטית.

בינוניכ-11 דקות קריאהעודכן 2026-10-30

נקודות מפתח לתכנון חוזי נתונים ב-JSON API

שישה דברים קשים מאוד לשינוי אחרי שחרור: מעטפות תשובה, פורמטי שגיאה, צורת עימוד, ייצוג מזהים וזמן, הסמנטיקה של null מול היעדר, ואבולוציית גרסאות.

בינוניכ-13 דקות קריאהעודכן 2026-10-30

בלי additionalProperties, אימות הסכמה לא מאמת כלום

עמידה באימות לא אומרת שהנתונים נקיים. Pydantic מתעלם משדות לא מוכרים כברירת מחדל, Zod מסיר אותם, Ajv מרשה להם לעבור — פרמטרים מומצאים ומפתחות עם שגיאת הקלדה עוברים בשקט. המאמר מציג שלוש צורות של קבלה שקטה ונותן מתכונים מלאים: לסגור אובייקטים, לשלוט בהמרת טיפוסים ולהדק כל רמת קינון.

Intermediateכ-8 דקות קריאהעודכן 2026-11-02