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

שורה תחתונה: נעלו קודם את $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.
שלוש תפיסות שגויות נפוצות:
requiredדורש רק שהמפתח יקיים:{"name": ""}תקין. כדי לדחות מחרוזת ריקה חייבים להוסיףminLength: 1;additionalProperties: falseמגדיל בעיות תאימות: שדה מיותר אחד מלקוח מכשיל את כל המטען, כך שאבולוציה של ה-API כופה על כל הקוראים לשדרג בתיאום;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 בכל ענף.