JSON Toolkit

בדיקות חוזה עם JSON Schema: ריתוך כללי האימות אל ה-CI

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

בינוניכ-8 דקות קריאהפורסם 2026-11-05עודכן 2026-11-05
דיאגרמת בדיקות חוזה JSON Schema ושערי אימות ב-CI

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

כשכללי האימות נשברים, אף אחד לא זורק חריגה: הרחבת הקבלה אף אחד לא שם לב אליה, והידוק הדחייה כל הצוות משלם עליה. סכמה היא לא קובץ קונפיגורציה — היא קוד שאף אחד לא בודק.

1. הסכמה נשברת — מי מגלה ראשון?

נתחיל במלכודת שדרכנו עליה באמת. הגדירו סכמה לפי דראפט 2020-12 וקמפלו אותה עם החבילה המובנית של Ajv: החריגה מקפצת מיד:

Error: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"

נקודת הכניסה המובנית של Ajv 8 מכירה רק draft-07; ל-2020-12 צריך לייבא מ-ajv/dist/2020. מלכודת נוספת: במצב strict, לכתוב format: "email" בלי להתקין ajv-formats מפיל את ההידור:

Error: unknown format "email" ignored in schema at path "#/properties/email"

לראות את השגיאות האלה מקומית בהידור הראשון זה בסדר. הבעיה: הרבה פרויקטים דוחים את ההידור עד שהבקשה מגיעה, וסכמה שבורה ממתינה לקורבן הראשונה שלה. ערמומי עוד יותר הוא הסכמה חצי-שבורה: required שנכתבה בטעות כמחרוזת במקום מערך זורקת schema is invalid: data/required must be array בהידור — אבל אם השלב הזה אף פעם לא רץ ב-CI, היא נשארת מוטלת בייצור.

לכן השער הראשון לא מאמת נתונים: הוא מאמת את הסכמה עצמה. פקודה אחת:

node scripts/lint-schemas.mjs
// scripts/lint-schemas.mjs
import { readFileSync, readdirSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv);
for (const file of readdirSync('schemas')) {
  const schema = JSON.parse(readFileSync(`schemas/${file}`, 'utf8'));
  ajv.compile(schema);
  console.log(`ok ${file}`);
}

מדוע לא npx ajv compile: לחבילת ajv v8 אין CLI, לכן npx ajv נופל עם could not determine executable to run; ה-CLI נמצאת בחבילה נפרדת ajv-cli (במדידה שלנו 5.0.0). אבל ajv-cli 5 תומך רק ב-draft-07 וב-draft-2019-09 — מול סכמת 2020-12 מהסעיף הראשון הוא זורק בדיוק את אותו no schema with key or ref "https://json-schema.org/draft/2020-12/schema". לכן פרויקט 2020-12 מריץ lint דרך אותו Ajv2020 שבמאמר; npx ajv-cli compile -s '**/*.schema.json' --strict מתאים רק לפרויקטי draft-07.

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

2. הטריו: חיובי, שלילי, סנאפשוט

שלד בדיקות הסכמה כולל שלוש חתיכות.

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

פיקסטורות שליליות: לכל אילוץ בסכמה, לפחות קלט אחד שמפר אותו. ל-minimum: 0 את הזוג -1 (נדחה) ו-0 (עובר); ל-enum ערך מחוץ לרשימה; ל-additionalProperties: false שדה מיותר. שליליות כבדות יותר מחיוביות: סכמה שמעבירה בשקט נתונים רעים היא באג שקט שמזהם עד מסד הנתונים.

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

schemas/order.schema.json
fixtures/order/valid-minimal.json        # צריך לעבור
fixtures/order/valid-full.json           # צריך לעבור
fixtures/order/invalid-type.json         # צריך להידחות
fixtures/order/invalid-extra-field.json  # צריך להידחות

מודול node:test המובנה ב-Node מספיק:

import { test } from 'node:test';
import { readFileSync, readdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(
  JSON.parse(readFileSync('schemas/order.schema.json', 'utf8'))
);

for (const f of readdirSync('fixtures/order')) {
  const data = JSON.parse(readFileSync(`fixtures/order/${f}`, 'utf8'));
  if (f.startsWith('valid-')) {
    test(`${f} צריך לעבור`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
  } else {
    test(`${f} צריך להידחות`, () => { assert.ok(!validate(data)); });
  }
}

סנאפשוט: סדרו את validate.errors לקובץ והשוו. מישהו משנה אילוץ בטעות — הסנאפשוט מאדים מיד. ב-Jest/Vitest שורה אחת: expect(errors).toMatchSnapshot(); ב-CI העבירו תמיד --ci: סנאפשוט חדש אז הוא כשל ולא נוצר בשקט, וסחף לא מקבל מחילה.

3. אספו את כל השגיאות במעבר אחד

הגדרה שקל לשכוח כשבודקים את השליליות: המאמת עוצר כברירת מחדל בשגיאה הראשונה. הנתונים שבורים בשלושה מקומות, אתם רואים אחד, מתקנים, מריצים שוב — וכך הלאה. עם allErrors: true (Ajv) או iter_errors (Python), הכול נראה בבת אחת:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(bad), key=lambda e: list(e.absolute_path))
for e in errors:
    print(list(e.absolute_path), e.validator, e.message)

ופייתון גם שומר עובדה נגד האינטואיציה: השגיאה ש-validate() מדווח עליה היא לא "הראשונה" אלא בחירת best_match. במדידה שלנו, עם שלוש הפרות (טיפוס id שגוי, role מחוץ לרשימה, שדה extra מיותר), דווח רק additionalProperties — שגיאת הטיפוס של id נשארה חבויה. ב-CI השתמשו תמיד ב-iter_errors כדי לקבל את התמונה המלאה.

והמלכודת הקלה עוד יותר לשכוח: format אינו אסרשן כברירת מחדל. דראפט 2020-12 מסווג פורמט כאנוטציה: ה-jsonschema של פייתון לא מאמת אותו בלי להעביר format_checker — בבדיקה שלנו "not-an-email" עבר בחופשיות. Ajv דורש להתקין ולרשום את ajv-formats. אם הפורמט חשוב (אימייל, תאריך, uuid), חייבת להיות פיקסטורה שלילית שמוכיחה שהקונפיגורציה של המאמת שלכם באמת בודקת אותו.

4. קמפלו פעם אחת — אל תתייחסו למהדר כאל מוצר חד-פעמי

מדידה מקומית (Node v22.13.1, Ajv 8.17, אותה סכמה מקוננת בשלוש רמות, אותם נתונים מפרים):

גישה זמן לעשרת אלפים אימותים
בכל פעם new Ajv() ואז compile 42233 מילישניות
מופע ברמת המודול + compile פעם אחת 6.9 מילישניות

גורם 6000. מאמת בתוך request handler שיוצר Ajv חדש בכל קריאה הוא תאונת הביצועים הנפוצה ביותר. שימו לב גם לתשובה השגויה הנפוצה: "ajv.validate(schema, data) מהדר מחדש בכל פעם". נמדד: מאה אלף קריאות לקחו 8.2 מילישניות — קרוב ל-11.2 מילישניות של פונקציה שמודרת פעם אחת, כי Ajv שומר סכמות במטמון פנימי. המחלה האמיתית היא לא cache miss, אלא מופע שלא נעשה בו שימוש חוזר. בפייתון זה דומה: Draft202012Validator עם שימוש חוזר מהיר פי ~5000 מקריאת validate() בכל פעם (2000 איטרציות: 0.3 מילישניות מול 1566 מילישניות), כי האחרונה בודקת את הסכמה עצמה בכל קריאה.

5. רתמו את השערים אל ה-CI: ארבע דלתות

מסודרים מהזול ליקר: אם הראשון נופל, השניים שאחריו מיותרים:

שער צורת הפקודה מה הוא תופס עלות
lint לסכמה node scripts/lint-schemas.mjs שגיאות תחביר, מילות מפתח מיושנות מילישניות
אימות פיקסטורות חבילת הבדיקות בסעיף 2 חוסר סנכרון פיקסטורה/סכמה, אילוצים מסומנים לא נכון מילישניות
diff של שינויים שוברים json-schema-diff-validator מול main required שהוסר, enum שהוצר, pattern שהוקשה שניות
אימות תגובות אמיתיות בדיקות עשן מול staging עם אסרשן .toMatchSchema() סחף של נקודות קצה אמיתיות שניות

קונפיגורציית מינימום ל-GitHub Actions:

- run: npm ci
- run: npm run lint:schemas        # Ajv2020 lint
- run: npm run test:schema         # טריו הפיקסטורות
- run: npx jest --ci --testPathPattern=schema   # שומר הסחף של הסנאפשוטים

שער השינויים השוברים ראוי להדגשה נפרדת: כלים כמו json-schema-diff-validator מסמנים באדום «הוסר required» ו«חסרים ערכי enum» כבר בשלב הרוויו — בדיוק שני השינויים ש-API-diff רגיל נוטה לדרג כ"לא שובר". הסרת שדה תגובה בלתי מזיקה לצרכן סלחני וקטלנית לצרכן עם switch ממצה.

6. אסרו על אובייקט השגיאה, לא רק בוליאני

בדיקה שלילית שמסתפקת באסרשן validate(data) === false לא מספיקה. עבור אותה סכמה, ודאו שהיא דוחה מהסיבה הנכונה: אילוץ זר יכול לעבור במקרה. לאובייקט השגיאה של Ajv יש אסימטריה מפורסמת: כש-required נכשל, instancePath מצביע על אובייקט האב, ושם השדה החסר מסתתר ב-params.missingProperty. נמדד כש-id חסר:

{ "instancePath": "", "schemaPath": "#/required",
  "keyword": "required", "params": { "missingProperty": "id" } }

האסרשן נכתב "קיימת שגיאה עם keyword=required ו-missingProperty=id", ולא עמום errors.length > 0. פירוק מלא של הסמנטיקה של שדות השגיאה נמצא בשגיאות JSON Schema שאי אפשר לקרוא: מיקום השגיאה ב-Ajv וטיפול בהודעות.

7. מלכודות נפוצות — טבלה מהירה

מלכודת תוצאה תרופה
לבדוק רק את ה-200 של המסלול המאושר סחף של תגובות שגיאה חומק סכמות ופיקסטורות גם לתגובות שגיאה
סכמות מגנרטור בלי רוויו required שהוסקה מדוגמה אחת דוחה נתונים אמיתיים; הצוות מכבה את האימות רוויו אנושי ל-required/enum אחרי ג'נרוט
סכמה גלובלית אחת לכל הגרסאות ההבדלים v1/v2 נמחקים גרסו את הסכמות יחד עם נקודות הקצה
dump של ה-payload המלא ללוג בכישלון PII וסודות דולפים ללוג ה-CI כתבו רק נתיבי JSON Pointer וקטעים מקוצרים
enum קפואה שינוי שם במוצר (canceled→cancelled) מפיל את האימות שינויי enum עוברים רוויו תאימות
גרסת המאמת לא ננעלה התנהגות שונה בין מכונות נעלו את גרסת המאמת ב-CI
להחליף בדיקות הרשאה 403 יכול להתאים לסכמת השגיאה והלוגיקה של התפקידים נשארת שגויה הסכמה שומרת על המבנה; הרשאה נבדקת בנפרד
לשכוח את הסמנטיקה של format זבל אימייל עובר בחופשיות התקינו ajv-formats / העבירו format_checker, הוסיפו שליליות

8. צ'קליסט יישום

  1. קבצי סכמה בניהול גרסאות, node scripts/lint-schemas.mjs בתוך הלינט;
  2. לכל סכמה לפחות פיקסטורה חיובית אחת ואחת שלילית לכל אילוץ; שם הקובץ הוא האסרשן;
  3. אובייקט השגיאה לסנאפשוט; ב-CI העבירו --ci כדי שהסנאפשוט לא יסחף לבד;
  4. מופע המאמת מנוצל מחדש ברמת המודול — אל תעשו new בתוך ה-handler;
  5. diff של שינויים שוברים תלוי על ה-PR; שינויי required/enum ברוויו מחייב;
  6. ה-PR הראשון מוכיח במכוון שהשער יכול ליפול: הסירו required, ראו אדום, החזירו.

בנושא קפדנות האימות (מתי סוגרים additionalProperties) מדובר בנושא אחר — ראו additionalProperties פתוחה: האימות של הסכמה לא שווה. המאמר הזה על לוודא שהכללים שנכתבו אפשר לסמוך עליהם תמיד. ליסודות, התחילו מ-JSON Schema מאפס ועד הנדסה.

FAQ

מה הקשר בין אימות סכמות לבדיקות חוזה בסטייל Pact?

זו חלוקת עבודה. אימות סכמה עונה "האם המסמך הזה נכון מבחינה מבנית" — אסרשן חד-כיווני. בדיקות חוזה בהובלת צרכן (Pact) עונות "האם הספק עוד עומד בצרכים הספציפיים של הצרכן X" ודורשות אימות דו-כיווני. צוות קטן מתחיל בשערי הסכמה, ומאמץ Pact כשמספר השירותים גדל.

כמה בדיקות שליליות מספיקות?

שלילית אחת לכל אילוץ היא הרצפה: כמות פיקסטורות שליליות בהתאם למספר האילוצים בסכמה (required, type, minimum, enum, pattern, additionalProperties…), בתוספת זוגות ערכי גבול נדחה/מתקבל. פחות שליליות מאילוצים אומר שאילוץ אחד בלי שמירה.

אילו שינויי סכמה נחשבים "שוברים"?

הערך מנקודת מבט הצרכן: מחיקת שדה תגובה, מחיקת enum או צמצומו, הקשחת pattern, הוספת שדה חובה לבקשה — הכול שובר; הוספת שדה אופציונלי לתגובה בדרך כלל בטוחה — בתנאי שהמפענח של הצרכן סובל מפתחות זרים. אוטומטו את ההשוואה עם json-schema-diff-validator ב-PR, ואת האזורים האפורים תחליטו בעצמכם.

לבדוק מול API אמיתי או מוקים?

שניהם, בשכבות: בדיקות הפיקסטורות לא נוגעות ברשת, רצות במילישניות, ורצות בכל PR; האימות האמיתי מכה ב-staging, עולה שניות, שייך לצינור העשן, ומדולג ב-skipif בלי אישורים. צוות שרץ ב-CI רק מול API אמיתי נחנק על ידי רשת מתנדנדת ומגבלות קצב, ובסוף מכבה את הבדיקות.

מקורות

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

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

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

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

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

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

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