בודק JSONPath

בדיקת שאילתות JSONPath לפי RFC 9535 מול JSON: סלקטורים, פילטרים, כל חמש הפונקציות, והנתיב המנורמל של כל התאמה — הכול בדפדפן שלך.

שאילתת JSONPath
JSON
התאמות: 2
  • נתיב$['store']['book'][0]['title']
    "Sayings of the Century"
  • נתיב$['store']['book'][2]['title']
    "Moby Dick"

שפת שאילתות שסוף-סוף יש לה תקן

JSONPath היא ל-JSON מה ש-XPath הוא ל-XML: שפה קטנה להצבעה על חלקים במסמך. כותבים ביטוי כמו $.store.book[0].title והוא בוחר את הצמתים התואמים. הרעיון בא מפוסט בלוג של Stefan Goessner מ-2007, ובמשך שבע-עשרה שנה הפוסט הזה היה ההפניה היחידה — מה שאמר שכל ספרייה מימשה את הפערים אחרת. מה בוחר $.. בודד? האם $[1,2] מובטח לחזור לפי הסדר? איך פילטר משווה ערך שאינו קיים? שאלו שלוש ספריות JSONPath ואולי תקבלו שלוש תשובות.

RFC 9535, שפורסם ב-2024, סוף-סוף קיבע את כל זה. הבודק הזה מכוון לתקן הזה, לא לפולקלור. הוא מריץ את השאילתה כפי שה-RFC אומר, מציג לכם כל התאמה יחד עם מיקומה המדויק, ו — חשוב — מסרב לשאילתה שתקפה בדיאלקט ישן אך לא ב-RFC 9535, ואומר לכם היכן הבעיה במקום לעשות בשקט משהו לא-תקני שכלי אחר היה עושה אחרת.

מקטעים וסלקטורים

שאילתה היא מזהה השורש $ ואחריו רצף מקטעים, וכל מקטע מחיל סלקטור אחד או יותר על הצמתים שהוא מקבל. יש חמישה סלקטורים:

  • שם — $.store או $["store"] בוחר את ערך המפתח. צורת הנקודה היא קיצור; צורת הסוגריים-והמרכאות עובדת לכל מפתח, כולל מפתח עם רווחים או פיסוק.
  • Wildcard — * בוחר כל מפתח של אובייקט או כל איבר של מערך.
  • אינדקס — [0] בוחר איבר מערך, ואינדקס שלילי סופר מהסוף, כך ש-[-1] הוא האחרון.
  • חיתוך (slice) — [start:end:step] בוחר טווח, בדיוק כמו ב-Python: [1:3] הם האיברים 1 ו-2, [::-1] הופך, [::2] לוקח כל שני.
  • פילטר — [?<ביטוי>] שומר רק את האיברים או המפתחות שעבורם הביטוי אמת (מוסבר בהמשך).

סוגריים יכולים להכיל כמה סלקטורים בבת אחת: [0, 2, "title"] בוחר שלושה דברים במקטע אחד. ומקטע יכול להיות מקטע-בן (נקודה או סוגריים בודדים) או מקטע-צאצא (..), שמחפש בצומת ובכל צאצאיו — $..author מוצא כל author בכל מקום במסמך.

פילטרים, וארבע הדרכים להשוות כלום

סלקטור פילטר בודק כל איבר עם ביטוי לוגי, שבו @ מפנה לאיבר הנוכחי ו-$ למסמך כולו. הביטוי יכול להשוות ערכים (==, !=, <, <=, >, >=), לשלב בדיקות עם && ו-|| ו-!, ופשוט לבדוק קיום: $..book[[email protected]] שומר את הספרים שיש להם ISBN, מפני ש[email protected] בוחר צומת רק כשהמפתח קיים.

החלק העדין הוא השוואת משהו שאינו קיים. שאילתה משמאל או מימין להשוואה מניבה ערך או כלום. ה-RFC מגדיר זאת במדויק: כלום שווה לכלום, כלום אינו שווה לשום ערך ממשי, וכל בדיקת סדר (<, >) מול כלום היא פשוט שקר. כך ש[email protected] < 10 מדלג בשקט על איבר שאין לו price במקום לשגות — מה שבדרך כלל רצוי, ותמיד מה שהתקן אומר.

חמש הפונקציות

RFC 9535 מוסיף חמש פונקציות שאפשר לקרוא בתוך פילטר:

  • length() — האורך של מחרוזת (נספר בתווים), מערך או אובייקט.
  • count() — כמה צמתים שאילתה בוחרת, כך שאפשר לפלטר לפי מונה: [?count(@.chapters) > 3].
  • value() — הערך היחיד ששאילתה בוחרת, או כלום אם היא בוחרת אפס או רבים.
  • match() — האם מחרוזת תואמת ביטוי רגולרי במלואה.
  • search() — האם ביטוי רגולרי נמצא במקום כלשהו במחרוזת.

התקן מחמיר לגבי אופן השימוש בהן, והבודק הזה אוכף זאת בזמן הפירסור: length() לוקחת ערך אחד בדיוק, כך ש-length(@.*) — שאילתה שיכולה לבחור צמתים רבים — היא שגיאת תחביר, לא שאילתה שמתנהגת בשקט לא כשורה. באופן דומה match() מחזירה אמת/שקר, כך שכתיבת match(@.a, "x") == true נדחית, מפני שתוצאה לוגית אינה משהו שמשווים.

הביטויים הרגולריים הם I-Regexp

match() ו-search() אינן משתמשות בביטויים רגולריים של JavaScript; הן משתמשות ב-I-Regexp (RFC 9485), תת-קבוצה קטנה ונישאת שתוכננה להתנהג אותו הדבר בכל שפה. רובה כמצופה — מחלקות תווים, כמתים, חלופות, מאפייני Unicode כמו \p{Lu} לאות רישית. המלכודת האחת היא הנקודה: ב-I-Regexp, . תואמת כל תו למעט תו חזרה לתחילת שורה (CR) או שורה חדשה (LF), מה שאומר שהיא כן תואמת את מפרידי השורה של Unicode U+2028 ו-U+2029 שנקודה של JavaScript מחריגה. הבודק הזה מהדר I-Regexp בנאמנות, כך שדפוס מתנהג כאן כפי ששרת תואם היה מעריך אותו.

ההבדל בין שתי הפונקציות הוא רק עיגון: match דורשת שכל המחרוזת תתאים, בעוד search מחפשת את הדפוס במקום כלשהו בתוכה. match(@, "a.*") מקבל "abc"; search(@, "b") מקבל כל מחרוזת המכילה b.

נתיבים מנורמלים, וריצה בדפדפן

עבור כל התאמה, הבודק הזה מציג Normalized Path — המיקום הקנוני שה-RFC מגדיר, כתוב בצורת הסוגריים-והמרכאות: $['store']['book'][0]['author']. בשונה מהשאילתה, שעשויה לבחור צמתים רבים, נתיב מנורמל מצביע על אחד בדיוק, בעזרת סלקטורים של שם ואינדקס בלבד וסגנון ציטוט קבוע. זו התשובה ל"מאיפה ההתאמה הזו הגיעה", וזה מה שמאפשר להפוך תוצאת wildcard בחזרה לקבוצת מיקומים קונקרטיים. רוב הבודקים מציגים את הערכים ומשאירים לכם לחשב את הנתיבים; זה מציג את שניהם.

כל המנוע רץ בדפדפן שלכם — ה-JSON מנותח והשאילתה מוערכת על המכשיר שלכם, ושום דבר שתדביקו לא מועלה, נשמר או נרשם. הוא מאומת מול חבילת בדיקות התאימות הרשמית של JSONPath, כך שתשובותיו מסכימות עם התקן ולא עם פרשנות של ספרייה אחת אליו.

שאלות נפוצות

לאיזה דיאלקט JSONPath זה משתמש?
ל-RFC 9535, התקן של IETF מ-2024, והוא מאומת מול חבילת בדיקות התאימות הרשמית של JSONPath. הוא במכוון אינו מקבל את מוסכמות Goessner הישנות במקומות שבהם הן חורגות מה-RFC; שאילתה כזו נדחית עם מיקום הבעיה כדי שתוכלו לתקן.
למה השאילתה שלי נדחתה כשהיא עובדת בכלי אחר?
מפני שהכלי הזה עוקב אחר מוסכמות Goessner שלפני-התקן, השונות מ-RFC 9535 בכמה מקומות — $.. בודד, ביטויי-script כמו [(@.length-1)], אינדקסים עם אפס מוביל, ושמות לא-מצוטטים עם תווים מיוחדים כולם לא-תקניים. ה-RFC החליף אותם במקבילות מוגדרות היטב, והבודק הזה נצמד ל-RFC.
מהו נתיב מנורמל?
המיקום הקנוני של צומת יחיד, כתוב בצורת הסוגריים-והמרכאות שה-RFC מגדיר, למשל $['store']['book'][0]['author']. שאילתה יכולה להתאים לצמתים רבים; לכל התאמה יש נתיב מנורמל אחד בדיוק, ולכן הבודק מציג אותו לצד כל ערך.
איך פילטר מתייחס לערך חסר?
ככלום, עם כללים שה-RFC מקבע: כלום שווה לכלום, כלום אינו שווה לשום ערך ממשי, וכל השוואת סדר (<, >) שכרוכה בכלום היא שקר. כך ש[email protected] < 10 פשוט מדלג על איבר ללא price במקום לזרוק שגיאה.
האם match() ו-search() משתמשות בביטויים רגולריים של JavaScript?
לא. הן משתמשות ב-I-Regexp (RFC 9485), תת-קבוצה נישאת. ההבדל המעשי העיקרי הוא הנקודה, שתואמת הכול למעט תו חזרה לתחילת שורה ושורה חדשה — כולל U+2028 ו-U+2029, שנקודה של JavaScript מחריגה. הבודק הזה מהדר I-Regexp בנאמנות כך שהתוצאות תואמות מימוש תקני.
מה ההבדל בין match ל-search?
עיגון. match() דורשת שכל המחרוזת תתאים לדפוס, כאילו הוא עטוף בעוגנים; search() מצליחה אם הדפוס נמצא במקום כלשהו במחרוזת. כל השאר בשתיהן זהה.
האם ה-JSON שלי נשלח לשרת?
לא. המסמך מנותח והשאילתה מוערכת כולה בדפדפן שלכם, ושום דבר שתדביקו לא מועלה או נרשם. בטוח לבדוק מול תגובת API אמיתית או קובץ הגדרות.