שאלות נפוצות על שאילתות ואימות: JSONPath ו-Schema
שורה תחתונה: האם שאילתות נתיב צריכות להשתמש ב-JSONPath או ב-Pointer, איך בוחרים בין Patch ל-Merge Patch, מה Schema יכול לחסום, ואיך בוחרים גרסת draft — הכל מוסבר במעבר אחד.

שורה תחתונה: השתמשו ב-JSONPath כדי למצוא צמתים לפי תנאי, ב-JSON Pointer או ב-JSON Patch כדי לכתוב לכתובת מדויקת ולערוך אותה, וב-Merge Patch כדי למזג קונפיגורציה; הצמידו Schemas חדשים ל-2020-12 והפעילו allErrors כדי שכל כשל יחזור במעבר אחד.
שאילתה היא "למצוא"; אימות הוא "לוודא שזה נכון". לכל אחד יש תקן משלו, אז אל תערבבו ביניהם.
1. שאילתות נתיב
איך בוחרים בין JSONPath ל-JSON Pointer?
זה תלוי אם אתם "מוצאים לפי תנאי" או "שולפים לפי מיקום".
| ממד | JSONPath | JSON Pointer |
|---|---|---|
| תקן | RFC 9535 (2024) | RFC 6901 (2013) |
| ביטוי | $.users[?(@.age > 18)].id |
/users/0/id |
| תווים כלליים רקורסיה | תומך ב-*, .. |
לא נתמך |
| סינון מותנה | נתמך | לא נתמך |
| תוצאה | קבוצת צמתים (אולי כמה) | מיקום יחיד |
| כושר ביטוי | חזק, אבל עם מורשת כבדה של הבדלי מימוש | חלש, אבל דטרמיניסטי לחלוטין |
| שימוש טיפוסי | חילוץ לוגים, דיבוג, חקר נתונים | JSON Patch, כתובות קונפיגורציה, נתיבי שגיאה |
כלל הבחירה פשוט:
- צריך "להשיג את כל הצמתים שמתאימים לתנאי" → JSONPath;
- צריך "לנעוץ מיקום מדויק אחד ולהוסיף, להסיר או לשנות אותו" → JSON Pointer;
- השתמשו בהם יחד: מצאו את הצומת עם JSONPath ותארו את מיקומו עם Pointer.
אל תשתמשו ב-JSONPath לכתובות בחוזה API. JSONPath מחזיר קבוצה, ושלושת הענפים לתוצאה ריקה, בת אלמנט אחד ורבת אלמנטים הופכים את הפרוטוקול למסובך; Pointer אומר "או שהוא מצביע על מיקום קיים או שהוא לא קיים", מה שמתאים יותר לחוזה.
מה ההבדל בין $.. ל-$.*?
$.. הוא ירידה רקורסיבית שעוברת כל רמה; $.* מחזיר רק את הילדים הישירים של הרמה הנוכחית.
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ שני האובייקטים{id:1, child:{...}}ו-{id:3};$..id→ שלושה ערכים:1,2,3;$.*.id→1ו-3, בלי2המקונן.
ירידה רקורסיבית נוחה אבל יקרה: $.. עובר את כל העץ, שזה O(n) על מסמך גדול, והוא בקלות אוסף שדות עמוקים שלא התכוונתם אליהם.
אם רמה קונקרטית עובדת, הימנעו מ-...
האם סינון יכול להשוות מחרוזות? האם הוא יכול להתאים עם regex?
השוואת מחרוזות עובדת; regex תלוי במימוש.
תחביר סטנדרטי (RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // compare with another node
השוואת מחרוזות משתמשת בגרשיים כפולים (גרשיים בודדים מותרים גם ב-RFC 9535, אבל חלק מהמימושים הישנים מקבלים רק גרשיים כפולים).
השוואה מספרית פועלת לפי סמנטיקה מספרית; ערבוב מחרוזת עם מספר, כמו "10" > 9, הוא התנהגות בלתי מוגדרת בתקן, אז אל תסתמכו עליה.
לגבי regex, RFC 9535 מגדיר את הפונקציות match() ו-search(), אבל התמיכה בהן משתנה מאוד בין מימושים.
הגישה הבטוחה: תנו ל-JSONPath לעשות סינון גס, ועשו את התאמת ה-regex בקוד אחרי שמקבלים את התוצאות.
האם להשתמש ב-JSON Patch או ב-Merge Patch?
שניהם מתארים "איך לשנות משהו", אבל כושר הביטוי והסמנטיקה שלהם שונים לחלוטין.
| ממד | JSON Patch (RFC 6902) | Merge Patch (RFC 7386) |
|---|---|---|
| צורה | מערך פעולות [{"op":"add",...}] |
אובייקט חלקי |
| פעולות נתמכות | add / remove / replace / move / copy / test | מיזוג בלבד (מחיקה דרך null) |
| כתובת לאלמנטי מערך | כן, לפי אינדקס | לא, המערך כולו מוחלף |
| אטומיות | test מאפשר נעילה אופטימיסטית |
אין |
| גודל מסמך | גדול אבל מדויק | קטן אבל מוגבל בכושר ביטוי |
| מתאים ל | עריכות עדינות, עדכוני API חלקיים | מיזוג קונפיגורציה, שליחת טופס חלקית |
קריטריונים להחלטה:
- לשנות שדה של האלמנט השלישי במערך → JSON Patch;
- למזג כמה רשומות קונפיגורציה לתוך הקונפיגורציה הקיימת → Merge Patch;
- לקבל בקרת מקביליות של "היכשל אם הערך המקורי אינו X" →
testשל JSON Patch.
שימו לב למלכודת בסמנטיקת ה-null של Merge Patch: null פירושו "מחק את המפתח הזה", לא "קבע את הערך ל-null".
אי אפשר לקבוע ערך ל-null עם Merge Patch.
2. אימות Schema
אילו בעיות JSON Schema פותר?
הוא הופך את "איך הנתונים אמורים להיראות" מהסכמה מילולית לחוזה שניתן לבדוק במכונה. השגיאות שהוא באמת יכול לחסום:
- שדות חובה חסרים (
required); - טיפוס שגוי, למשל מחרוזת במקום מספר (
type); - ערכים מחוץ לטווח (
minimum/maximum/minLength); - ערכים מחוץ לקבוצה המותרת (
enum); - טיפוסי אלמנטים לא עקביים במערך או אורך מערך שגוי (
items/minItems); - שדות חסרים במבנים מקוננים (אימות רקורסיבי דרך
properties); - שדות עודפים (
additionalProperties: false).
מה שהוא לא יכול לפתור: כללים עסקיים ("זמן הסיום חייב להיות אחרי זמן ההתחלה"), תנאים חוצי-שדות (חלקם אפשר לבטא עם if/then,
אבל הקריאות נפגעת), ייחודיות במסד נתונים ובדיקות הרשאה.
אז Schema הוא שומר הסף של "צורת הנתונים", לא תחליף ללוגיקה עסקית.
האם להשתמש ב-draft-07 או ב-2020-12?
השתמשו ב-2020-12 לפרויקטים חדשים; במערכות קיימות, בדקו מה הוולידטור תומך בו.
| גרסה | מצב | שינויים מרכזיים |
|---|---|---|
| draft-04 | מיושן | הרבה ספריות ישנות עדיין משתמשות בו כברירת מחדל |
| draft-07 | תקן דה-פקטו | התמיכה הרחבה ביותר, האקוסיסטם העשיר ביותר |
| 2019-09 | מעבר | הציג $defs, unevaluatedProperties |
| 2020-12 | מומלץ כיום | items פוצל ל-prefixItems + items, תומך ב-$dynamicRef |
שתי מלכודות מעבר:
- תחביר tuple השתנה: ב-draft-07
itemsעם ערך מערך ביטא tuple; 2020-12 העביר זאת ל-prefixItems; $refומילות מפתח אחיות: ב-draft-07 מילות מפתח אחיות מתעלמות כשקיים$ref; 2020-12 מאפשר להן לדור יחד.
אם הוולידטור תומך רק ב-draft-07, כתיבת תחביר 2020-12 בכל זאת מתעלמת בשקט — והמקרה המסוכן ביותר הוא "האימות עובר" כשבעצם שום דבר לא אומת.
איך מייצרים טיפוסי TypeScript מ-Schema?
שני מסלולים: גזרו טיפוסים מה-Schema, או ייצרו את ה-Schema מהטיפוסים.
מ-Schema לטיפוסים (הבקאנד מגדיר את החוזה, החזית צורכת אותו):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
המפתח הוא as const: בלעדיו, type: 'string' מוסק כ-string ולא כליטרל 'string', וההיסק מתפרק.
מטיפוסים ל-Schema (לחזית כבר יש טיפוסים והיא צריכה אימות בזמן ריצה):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
היתרון של המסלול הזה הוא ש"הגדרה אחת מניבה גם טיפוסים וגם אימות בזמן ריצה"; המחיר הוא הכנסת DSL,
וייתכן שה-Schema שנוצר נושא פרטי מימוש (למשל מבנה $ref לא אטרקטיבי).
איך מאתרים את השדה הספציפי כשהאימות נכשל?
הביטו ב-instancePath (2020-12) או ב-dataPath (draft-07) שהוולידטור מחזיר; זה JSON Pointer למיקום השגיאה.
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price" אומר לכם ישירות: בשדה price של האלמנט השלישי במערך items יש טיפוס שגוי.
שתי הצעות מעשיות:
- החזירו את כל השגיאות בבת אחת במקום לזרוק על הראשונה — תיקון הכל במעבר אחד בחזית מהיר יותר מניסיונות הלוך ושוב;
- המירו את ה-Pointer לשפה פשוטה לפני שמציגים אותו למשתמשי קצה;
/items/2/priceמועיל למפתחים וחסר תועלת למשתמשים.
allErrors: true הוא המתג ברוב הוולידטורים (Ajv דורש הפעלה מפורשת); ברירת המחדל היא לרוב מצב fail-fast.
האם להפעיל additionalProperties: false?
הפעילו אותו לחוזים פנימיים; היזהרו ב-API ציבוריים.
ברגע שהוא מופעל, כל שדה שלא הוכרז תחת properties נכשל באימות. היתרון הוא לתפוס טעויות הקלדה
(userNmae) והעברה לא צפויה; החיסרון הוא שהוספת שדה במעלה הזרם שוברת צרכנים במורד הזרם, ומשמידה תאימות לאחור.
פשרה:
| תרחיש | המלצה |
|---|---|
| בין שירותים פנימיים | הפעילו. שינויי שדות עוברים ניהול גרסאות |
| API חיצוניים ציבוריים | כבו. אפשרו למעלה הזרם להוסיף שדות |
| צריכת נתוני צד שלישי | כבו. אתם לא שולטים בצד השני |
| אימות קונפיגורציה | הפעילו. טעות הקלדה בקונפיגורציה צריכה להיכשל מיד |
איך מאמתים שאלמנטי מערך ייחודיים?
שתי סמנטיקות שונות — אל תבלבלו ביניהן:
{ "uniqueItems": true }
זה מסיר כפילויות של האלמנט כולו, ודורש שהמערך לא יכיל שני אלמנטים שווים לעומק.
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
הקטע שלמעלה רק מבטיח שהאובייקטים כשלם אינם כפולים; {"id":"a","name":"x"} ו-{"id":"a","name":"y"} לא נחשבים כפולים.
כדי להסיר כפילויות לפי שדה מסוים, Schema לא יכול לבטא זאת; צריך לאמת בשכבת העסקים:
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id 重复');
}
האם format באמת מאמת אימיילים ותאריכים?
לא בהכרח. במפרט, format הוא "הערה" כברירת מחדל ולא "הצהרה", כך שהוולידטור עשוי לאסוף אותו בלי לאמת אותו.
- Ajv דורש התקנה והגדרה של החבילה הנפרדת
ajv-formats, אחרתformat: "email"הוא למעשה חסר תוקף; - חלק מהוולידטורים מתייחסים ל-
formatכהצהרה שמופעלת כברירת מחדל, כך שההתנהגות לא עקבית; - גם כשמופעל, ה-regex של
emailרופף מאוד —a@bעובר.
מסקנה: אל תתייחסו ל-format כאימות חזק. עשו אימות קפדני של אימיילים, כתובות URL ותאריכים בקוד העסקי;
שמירת format ב-Schema כתיעוד שווה משהו, אבל אל תצפו שהוא יעצור נתונים מזוהמים.
3. עיון מהיר
| שאלה | מסקנה |
|---|---|
| מצא צמתים לפי תנאי | JSONPath |
| כתובת מדויקת, החלת טלאים | JSON Pointer |
| שנה אלמנטי מערך | JSON Patch, Merge Patch לא יכול |
| מזג קונפיגורציה | Merge Patch |
| גרסת Schema לפרויקטים חדשים | 2020-12, שימו לב ל-prefixItems |
| ייצור טיפוסי TS מ-Schema | json-schema-to-ts + as const |
| איתור שגיאות אימות | קראו instancePath, הפעילו allErrors |
| הסרת כפילויות לפי שדה | Schema לא יכול; השתמשו ב-Set בשכבת העסקים |
format: email |
רוב הוולידטורים לא בודקים אותו כברירת מחדל, אז אל תסתמכו עליו |