Schema Validation ל-APIs: איך בונים שכבת ולידציה שלא קורסת בפרודקשן
מאת צוות מדיה דיל · 01.08.2026 · API Engineering · 7 דק׳
מדריך ארכיטקטוני מעמיק ל-Schema Validation ב-APIs: JSON Schema מול Zod ומול Protobuf, Validation Layers, ולמה ולידציה חלשה היא לרוב שורש הבעיה בתקריות פרודקשן.
בשבוע שעבר צוות backend של חברת פינטק ישראלית גילה שלקוח שלח בשדה amount מחרוזת ריקה במקום מספר, וה-API "בלע" את זה בשקט כי הולידציה בדקה רק if (amount). התוצאה: רשומת עסקה עם סכום NaN שהתגלגלה עד למערכת ההנהלת חשבונות ועצרה תהליך התאמה שלם. זה לא סיפור נדיר. רוב תקריות הפרודקשן ב-APIs לא נובעות מבאג לוגי מתוחכם — הן נובעות מקלט שלא היה אמור לעבור את שכבת הכניסה, ובכל זאת עבר. Schema Validation היא לא "nice to have", היא קו ההגנה הראשון והחשוב ביותר של כל API.
למה Validation "ידנית" נכשלת בסקייל
ברוב הפרויקטים הצעירים הולידציה מתחילה כסדרת if סטטמנטים בתוך ה-handler: בדיקת required, בדיקת טווח, אולי regex לאימייל. זה עובד לזמן קצר, אבל נשבר משלוש סיבות מבניות. ראשית, הלוגיקה מתפזרת — כל endpoint מממש ולידציה משלו, ומה שמוגדר כ"חובה" ב-endpoint אחד לא נבדק באחר. שנית, אין מקור אמת יחיד (single source of truth) לצורת הנתונים, כך שה-frontend, ה-backend וה-API docs מתפצלים בהדרגה עד שאף אחד לא בטוח מהי הצורה האמיתית. שלישית, ולידציה ידנית לא מייצרת הודעות שגיאה עקביות — לקוחות API מקבלים 400 עם טקסט חופשי שמשתנה מ-endpoint ל-endpoint, מה שהופך אינטגרציה לסיוט.
הפתרון הארכיטקטוני הוא להפריד את הגדרת הצורה (schema) מהלוגיקה העסקית, ולתת למנוע ולידציה ייעודי לאכוף אותה באופן דקלרטיבי, לפני שקוד עסקי בכלל רואה את הבקשה.
שלוש גישות מרכזיות: JSON Schema, Zod ודומיו, ו-Protobuf
JSON Schema הוא תקן שפה-אגנוסטי לתיאור מבנה JSON: טיפוסים, שדות חובה, טווחים, תבניות regex, ו-composition (oneOf, anyOf). היתרון הגדול הוא שהוא ניתן לשימוש הן ב-backend (למשל דרך Ajv ב-Node) והן ב-frontend, וגם משמש כבסיס ל-OpenAPI. החיסרון: הוא JSON טהור, כלומר מסורבל לכתיבה ידנית לסכמות מורכבות, ואין לו type inference אוטומטי לשפת ה-backend.
Zod (ודומיו כמו Yup, Valibot ב-TypeScript) הפכו לסטנדרט דה-פקטו בעולם ה-TypeScript, כי הם מאפשרים להגדיר סכמה בקוד ולקבל ממנה טיפוס TypeScript אוטומטית עם z.infer. זה סוגר את הפער בין "מה שהקומפיילר חושב שהמבנה" לבין "מה שבאמת נבדק ב-runtime" — פער שמייצר המון באגים כי TypeScript עצמו לא מבצע שום בדיקה ב-runtime.
Protobuf (עם gRPC) הולך בכיוון שונה לגמרי: הסכמה מוגדרת ב-.proto, מקומפלת מראש, והנתונים עוברים ב-binary עם type safety חזק ברמת ה-wire protocol. זה מתאים לתקשורת service-to-service פנימית בסקייל גבוה, אבל פחות נוח ל-public APIs שצריכים קריאות אנושית ותאימות עם דפדפנים.
const CreateOrderSchema = z.object({
amount: z.number().positive(),
currency: z.enum(["ILS", "USD", "EUR"]),
customerId: z.string().uuid(),
items: z.array(z.object({
sku: z.string().min(1),
qty: z.number().int().positive(),
})).min(1),
});
type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
Trade-offs: איפה בודקים ומה זה עולה
ההחלטה הראשונה היא איפה בשרשרת הבקשה מתבצעת הולידציה. Validation ב-API Gateway (למשל דרך OpenAPI validator כ-middleware) עוצרת בקשות פסולות לפני שהן בכלל מגיעות לקוד העסקי — יתרון ברור לביצועים ולבידוד אחריות. אבל היא לא תמיד יכולה לאכוף חוקים עסקיים שתלויים ב-state (כמו "הכמות לא יכולה לעלות על המלאי הזמין"), שדורשים גישה למסד נתונים. לכן בפועל בונים שתי שכבות: Structural Validation גנרית בכניסה (טיפוסים, פורמט, שדות חובה) ו-Business Rule Validation בתוך שכבת השירות, שרצה אחרי טעינת ה-context הרלוונטי.
עלות הביצועים של ולידציה מודרנית זניחה כמעט תמיד — Ajv למשל מקמפל סכמות ל-JS פונקציות מהירות מאוד. הבעיה האמיתית היא עלות תחזוקה: סכמה שלא מתעדכנת יחד עם הקוד הופכת למכשול. לכן חשוב לייצר את הסכמה כמקור יחיד שגם ה-DB migration, גם ה-API docs, וגם ה-client SDK נגזרים ממנו — ולא הפוך.
החלטות עיצוב מרכזיות שקובעות הצלחה
ההחלטה החשובה ביותר היא Strict vs. Lenient parsing. במצב strict, כל שדה לא מוכר נדחה (additionalProperties: false); במצב lenient, שדות עודפים פשוט מתעלמים. Strict נשמע בטוח יותר, אבל שובר לקוחות ישנים בכל פעם שמוסיפים שדה חדש ל-payload — כי לקוח ישן ששולח שדה שכבר לא רלוונטי ייחסם. הפתרון המקובל הוא strict על תשובות (responses) חדשות, אבל lenient מבוקר על קלט, בשילוב עם ניטור של שדות לא מוכרים כדי לזהות דפוסי שימוש בלתי צפויים.
החלטה שנייה היא איך מנרמלים הודעות שגיאה. API שמחזיר {"error": "Invalid input"} חסר תועלת. הסטנדרט הנכון הוא RFC 7807 (Problem Details) עם רשימת שגיאות פר-שדה, כולל path (items[2].qty), קוד שגיאה יציב, והודעה קריאה לאדם. זה מה שמאפשר ל-frontend להציג שגיאות inline ליד השדה הרלוונטי, ולא רק Toast גנרי.
החלטה שלישית היא ניהול גרסאות סכמה. כשמוסיפים שדה חובה חדש, זה Breaking Change. הדרך הנכונה היא להוסיף אותו כ-optional עם ברירת מחדל, לתקשר deprecation, ורק בגרסת API הבאה (/v2/) להפוך אותו לחובה.
טעויות נפוצות בפרודקשן
- ולידציה רק ב-frontend — הנחה מסוכנת שכל תעבורה מגיעה מה-UI שכתבתם. כל endpoint חייב לאמת מחדש, כי curl, Postman, ו-actors זדוניים לא עוברים דרך ה-frontend.
- בדיקת טיפוס בלי בדיקת טווח — לוודא ש-
ageהוא מספר זה לא מספיק; בליmin(0).max(150)תקבלו רשומות עם גיל 999999 שיפוצצו דוחות אנליטיקה. - אי-טיפול ב-coercion שקטה — ספריות מסוימות (ובעיקר קלט מ-query strings) הופכות אוטומטית "123" למספר 123. זה נוח, אבל מסתיר מקרים שבהם הלקוח שלח בטעות מחרוזת שלא הייתה אמורה להתקבל.
- סכמות ש"מוותרות" על nested objects — ולידציה של האובייקט העליון בלבד, בלי לרדת ל-arrays ו-objects מקוננים, משאירה חורים ענקיים בדיוק במקום שהכי צריך הגנה.
- אין בדיקת idempotency key format — כשמשתמשים ב-idempotency keys (חשוב ל-endpoints כספיים), חייבים לאמת גם את הפורמט שלהם, אחרת אפשר לגרום להתנגשויות מכוונות.
מקרה מעשי: קפיצה בין שתי סביבות
דוגמה שממחישה למה זה משנה בפועל: חברת מסחר אלקטרוני עברה מ-Monolith עם ולידציה מפוזרת ל-Microservices, ובתהליך המעבר גילתה שבשירות ההזמנות הישן, שדה discount_percent נבדק כ"מספר בין 0 ל-100" בקוד אחד, אבל ב-endpoint אחר לגמרי (שנוסף שנה מאוחר יותר על ידי מפתח אחר) לא הייתה שום בדיקת טווח עליון, מה שאיפשר בפועל להזין הנחה של 250%. הבאג הזה שכב רדום כמעט שנתיים, כי אף אחד לא יצר הזמנה עם הנחה כזו בפועל — עד שסקריפט אינטגרציה אוטומטי, שנכתב על ידי שותף עסקי, שלח ערך שגוי במקרה, וגרם לזיכוי של אלפי שקלים ללקוח בודד. אחרי המעבר לסכמת Zod מרכזית אחת שמשותפת בין כל ה-endpoints הרלוונטיים (DiscountSchema מיובא בכל מקום שצריך אותו), הבעיה הזו הפכה מבנית בלתי אפשרית — כי יש מקום יחיד שמגדיר את הכלל, לא כמה עותקים שיכולים להתפצל.
Property-Based Testing מעל הסכמה
יתרון פחות מוכר של שכבת ולידציה מבוססת סכמה הוא שהיא פותחת דלת ל-Property-Based Testing: במקום לכתוב ידנית עשרות מקרי בדיקה, כלים כמו fast-check (JS) או Hypothesis (Python) יכולים לגזור אוטומטית ערכי קלט אקראיים מתוך הסכמה עצמה — כולל מקרי קיצון שמפתח אנושי כנראה לא היה חושב עליהם (מחרוזות ריקות, מספרים שליליים, Unicode חריג, מערכים ריקים). זה הופך את הסכמה ממסמך תיעוד פסיבי לכלי בדיקה אקטיבי: כותבים את הכלל פעם אחת ("amount הוא מספר חיובי"), והמערכת בודקת אוטומטית מאות תרחישים סביב הגבול הזה. בפרויקטים עם לוגיקה עסקית מורכבת סביב חישובים כספיים, זו לרוב הדרך היעילה ביותר לתפוס באגי Edge Case לפני שהם מגיעים לפרודקשן, בהרבה פחות זמן כתיבה מאשר בדיקות ידניות מקבילות.
ולידציה כחלק מ-CI/CD: מניעת Drift
הסיכון הגדול ביותר לאורך זמן הוא Schema Drift — מצב שבו הסכמה בקוד לא תואמת עוד למה שקורה בפועל במסד הנתונים או ב-API החיצוני שאיתו משתלבים. הדרך למנוע את זה היא לשלב את הסכמה בתהליך ה-CI: בדיקת Contract שמריצה כמה בקשות אמיתיות מול סביבת Staging ומוודאת שהתשובות אכן עומדות בסכמה המוגדרת, לא רק שהקוד "מתקמפל". כשיש שינוי סכמה, ה-Pull Request עצמו צריך להראות diff ברור — כלי כמו openapi-diff או git diff רגיל על קובץ הסכמה — כדי שסוקר הקוד יראה מיד אם מדובר בשינוי לא-שובר (הוספת שדה optional) או בשינוי שובר (הסרת שדה, שינוי טיפוס). ארגונים בשלים מוסיפים גם Gate אוטומטי ב-CI שחוסם merge של שינוי שובר בלי label מפורש של "breaking change" שמאלץ דיון מודע.
ולידציה של תשובות יוצאות, לא רק קלט נכנס
נקודה שלרוב מוזנחת: רוב הצוותים מתמקדים בולידציית קלט (Request Validation) ושוכחים לחלוטין את ולידציית הפלט (Response Validation). זה מפתיע, כי מבחינת הלקוח החיצוני, תשובה שלא תואמת את הסכמה המתועדת היא בדיוק אותה בעיה כמו קלט לא תקין — היא שוברת את החוזה. ולידציית תשובות בסביבת בדיקה (לא בהכרח בפרודקשן, בגלל עלות הביצועים) תופסת מקרים שבהם קוד עסקי משנה בטעות את מבנה הנתונים המוחזר — למשל שדה שהופך מ-number ל-string בגלל שינוי ב-ORM, בלי שאף אחד שם לב כי הבדיקות הקיימות בדקו רק שהתשובה "success: true" ולא בדקו את הצורה המדויקת של שאר השדות. הוספת שכבת ולידציה כזו כחלק מ-Integration Tests, שמריצה את אותה סכמה גם על תשובות יוצאות, סוגרת פינה משמעותית שנשארת פרוצה ברוב המערכות.
מתי כדאי להשקיע ומתי זה overkill
ל-internal microservices עם תעבורה נמוכה וצוות קטן, לפעמים Zod עם סכמות בסיסיות מספיק לגמרי — אין צורך ב-Ajv עם compilation מתקדם או ב-Protobuf. אבל ל-public-facing API עם לקוחות חיצוניים, תשלומים, או נתונים רגישים, ההשקעה בשכבת ולידציה מלאה (עם OpenAPI כמקור אמת, generation אוטומטי של client SDKs, ו-contract testing) מחזירה את עצמה כבר בחודש הראשון — כל תקרית שנמנעת שווה עשרות שעות דיבוג.
במדיה דיל, כשאנחנו בונים אינטגרציות API עבור לקוחות, השכבה הראשונה שאנחנו מגדירים היא תמיד סכמת הולידציה — לפני שנכתבת שורת לוגיקה עסקית אחת. זה נשמע כמו overhead, אבל בפועל זה מקצר משמעותית את זמן ה-QA ומצמצם דרסטית את מספר ה-hotfixes אחרי release.
סיכום
Schema Validation היא לא שכבת קוסמטיקה — היא החוזה בין המערכת שלכם לעולם החיצון. השקעה בכלי ולידציה נכון (Zod ל-TypeScript, JSON Schema ל-ecosystem שפה-אגנוסטי, Protobuf לתקשורת פנימית בסקייל), יחד עם מדיניות ברורה לגבי strict/lenient parsing, ניהול גרסאות, והודעות שגיאה עקביות — הן ההבדל בין API שאפשר לסמוך עליו לבין API שכל שדרוג בו הוא הימור.
תגיות: Schema Validation · JSON Schema · Zod · API Design · Protobuf · OpenAPI · Backend Validation