שפת השאילתות JSONPath בפועל
שורה תחתונה: ב-2024 קיבל JSONPath סוף סוף תקן רשמי, RFC 9535. המאמר מסביר את התחביר, סמנטיקת מסננים, פונקציות ואת ההבדלים ההיסטוריים בין מימושים.

שורה תחתונה: אתרו קודם עם נתיב מפורש וסננו על גביו, וכתבו ביטויים לפי RFC 9535; השתמשו בגישה לתכונות או ב-JSON Pointer עבור שדה קבוע יחיד, וב-jq או בקוד עבור אגרגציה.
בטח ראיתם ביטויים כמו
$.store.book[?(@.price < 10)].title, אבל הוא הפך לתקן בינלאומי רק ב-2024.
1. מתקן דה-פקטו לתקן רשמי
JSONPath הוצע על ידי Stefan Gössner ב-2007, ובמשך זמן רב ה"מפרט" היחיד היה פוסט בלוג אחד, כשכל מימוש שפה עשה מה שנראה לו:
- האם מסננים צריכים סוגריים (
?(@.a)מול?@.a); - האם אינדקס המערך
[-1]נתמך; - האם התוצאה מחזירה "ערכים" או "ערכים ונתיבים";
- האם ביטויי סקריפט (
?(@.price < $.max)) מותרים.
RFC 9535, שפורסם בפברואר 2024, איחד את הפערים הללו לתחביר וסמנטיקה אחת וסיפק חבילת מבחני תאימות רשמית. פרויקטים חדשים צריכים ללכת לפי RFC 9535; בהגירת פרויקטים ישנים, אשרו קודם את גרסת המימוש.
2. דף עזר לתחביר
| תחביר | משמעות | דוגמה |
|---|---|---|
$ |
צומת השורש | $.store |
@ |
הצומת הנוכחי בתוך מסנן | $[?@.price < 10] |
.name |
צומת בן (השם חייב להתאים ל-[A-Za-z_][A-Za-z0-9_]*) |
$.store.book |
['name'] |
צומת בן (כל שם, כולל תווים שאינם ASCII ונקודות) | $['store']['书'] |
[0] |
אינדקס במערך; מספרים שליליים נספרים מהסוף | $.book[0], $.book[-1] |
[0,2] |
כמה אינדקסים / כמה שמות | $.book[0,2] |
[1:3] |
פרוסה, התחלה כוללת וסוף בלעדי | $.book[1:3] |
[::2] |
פרוסה עם צעד | $.book[::2] |
[*] |
תו כללי לכל צמתי הבן | $.book[*].title |
..name |
ירידה רקורסיבית, חיפוש בכל עומק | $..price |
..[*] |
נטילת כל הצמתים באופן רקורסיבי | $..* |
[?<expr>] |
בורר מסנן | $.book[?@.price < 10] |
סמנטיקה אחת שחייבים לזכור: JSONPath מחזיר קבוצת צמתים (nodelist), לא ערך יחיד. גם כשרק צומת אחד מתאים, התוצאה היא רשימה באורך אחד.
3. מעבר על התחביר עם דוגמה אחת
{
"store": {
"book": [
{ "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
{ "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
{ "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"expensive": 10
}
| ביטוי | תוצאה |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
4 מחירים (כולל מחיר האופניים) |
$.store.book[*].title |
3 כותרות |
$.store.book[?@.price < 10].title |
כותרות הספרים 1 ו-3 |
$.store.book[?@.tags] |
כל הספרים שיש להם tags (בדיקת קיום) |
$.store.book[-1].title |
הכותרת של הספר האחרון |
$..book[0,1] |
שני הספרים הראשונים |
$.store.book[?@.price < $.expensive] |
תלוי מימוש מבחינה תחבירית; RFC 9535 אינו מתיר הפניות לשורש |
השורה האחרונה היא מלכודת היסטורית: מימושים רבים מתירים $.expensive בתוך מסנן להשוואה בין צמתים, מה ש-RFC 9535 אוסר במפורש —
מסנן יכול לגשת רק לצומת הנוכחי @. כשצריך להשוות לסף גלובלי, העבירו את הסף כארגומנט לפונקציית השאילתה.
4. מה מסננים יכולים לבטא
ביטויי מסנן של RFC 9535 תומכים בארבעה סוגי מבנים:
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
כמה דוגמאות מעשיות:
// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]
// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// More than one tag
$.store.book[?length(@.tags) > 1]
// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()
match() הוא התאמה של המחרוזת כולה ו-search() הוא חיפוש תת-מחרוזת; דיאלקט הביטוי הרגולרי שמתחת הוא I-Regexp (תת-קבוצה של RFC 9485),
שאינו תומך לא ב-backreferences ולא ב-lookaround — זה גם מגבלה וגם מגן נוח מפני regex DoS.
5. הבדלי מימושים: בדקו לפני הגירה
| מימוש | שפה | תמיכה ב-RFC 9535 | הערות |
|---|---|---|---|
jsonpath-plus |
JavaScript | תואם ברובו | תומך בפונקציות הרחבה ובביטויי סקריפט; יכולת גבוהה יותר אבל לא תקני לחלוטין |
jsonpath (dchester) |
JavaScript | חלקי | ספרייה ותיקה עם תחביר ?() ומבנה תוצאה שונה |
ojg / gjson |
Go | ל-ojg יש תמיכה טובה |
gjson משתמש בתחביר נתיב אחר ואינו JSONPath |
jsonpath-ng |
Python | חלקי | תומך בהרחבות ומתירן למדי |
Jayway JsonPath |
Java | חלקי | אקוסיסטם רחב; התנהגות היסטורית שונה מה-RFC |
שלוש בדיקות לפני הגירה:
- האם סוגריים במסנן חובה;
- האם אינדקסים שליליים כמו
-1נתמכים; - האם התוצאה היא "מערך ערכים" או "מערך אובייקטים
{path, value}".
השלישית היא הקלה ביותר להיתקל בה: הפרונט קורא result[0], ואחרי החלפת ספרייה הוא מקבל אובייקט, והעמוד מתרוקן.
6. ביצועים ואבטחה
ביצועים
- JSONPath הוא בעצם מעבר אחד עם מורכבות O(מספר צמתים), והוא לא יכול להשתמש באינדקסים;
- ירידה רקורסיבית עם
..היא היקרה ביותר; אם אפשר לאתר עם$.store.bookקודם ואז לסנן, אל תכתבו$..book; - קמפלו ביטויים פעם אחת והשתמשו בהם שוב (רוב הספריות מציעות
compileאו מטמון) במקום לפרסר בכל שאילתה; - במסמכים עם יותר מ-100,000 צמתים, שקלו חילוץ מדויק עם JSON Pointer קודם, או עיבוד מקדים בשרת עם
jq.
אבטחה
- לביטויים שמגיעים מקלט משתמש חייבים להיות גבולות אורך ועומק קינון;
- אל תתנו ל-JSONPath לנהל הרשאות — הוא יכול רק לקרוא, ולא יכול לבטא "אילו שדות אינם נראים";
- כשהביטוי הרגולרי שמשמש את
match()מגיע ממשתמשים, הגבילו את המורכבות שלו; למרות שהוא בטוח יותר מ-PCRE, עדיין מומלץ גבול אורך.
7. מתי לא להשתמש ב-JSONPath
- צריך רק שדה קבוע אחד → השתמשו ישירות ב-
data.store.bicycle.color; - צריך לכתוב בחזרה אחרי שינוי → השתמשו ב-JSON Pointer או בפאצ'; JSONPath לא כותב בחזרה;
- צריך אגרגציה (סכימה, קיבוץ) → השתמשו ב-
jq, ב-SQL או בקוד; JSONPath רק בוחר; - המסמך ענק וצריך רק כמה שדות → השתמשו בפרסר זורם במקום לקרוא את כל העץ לזיכרון קודם.
8. רשימת בדיקה
- [ ] הביטויים עומדים ב-RFC 9535 ואינם מסתמכים על הפניות לשורש;
- [ ] ברור שהתוצאה היא קבוצת צמתים; משתמשים ב-
[0]או ב-value()כדי לקבל ערך יחיד; - [ ] ביטויים מקומפלים מראש ונעשה בהם שימוש חוזר;
- [ ] לביטויים בשליטת משתמש יש מגבלות אורך ומורכבות;
- [ ] מקרי מבחן התאימות הרשמיים מורצים בעת החלפת מימוש.
שאלות נפוצות
איך בוחרים בין JSON Pointer ל-JSONPath?
השתמשו בגישה רגילה לתכונה או ב-JSON Pointer כשצריך שדה קבוע אחד; פנו ל-JSONPath כשצריך מסננים, תווים כלליים או ירידה רקורסיבית. JSONPath רק בוחר ולעולם לא כותב בחזרה, ולכן כתיבה בחזרה שייכת ל-JSON Pointer או לפאצ'.
למה מסנן לא יכול להפנות ל-$.expensive?
RFC 9535 אוסר במפורש הפניות לשורש; מסנן רשאי לגשת רק לצומת הנוכחי @. העבירו את הסף כארגומנט לפונקציית השאילתה במקום. מימושים ישנים מקבלים את התחביר, אבל הוא נשבר כשמחליפים או משדרגים ספרייה.
כדאי להסתמך על result[0] כדי לקרוא ערך?
רק אחרי שאישרתם שהמימוש הנוכחי מחזיר מערך ערכים. חלק מהספריות מחזירות אובייקטים {path, value}, ואז result[0] הופך לאובייקט והעמוד מתרוקן. קראו ערכים בודדים דרך [0] או value(), והריצו את חבילת התאימות הרשמית לפני החלפת מימושים.