مُختبِر 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"] يختار قيمة عضو. صيغة النقطة اختصار؛ وصيغة الأقواس وعلامات الاقتباس تعمل لأي مفتاح، بما في ذلك مفتاح فيه مسافات أو علامات ترقيم.
  • البدل — * يختار كل عضو في كائن أو كل عنصر في مصفوفة.
  • الفهرس — [0] يختار عنصر مصفوفة، والفهرس السالب يعدّ من النهاية، فـ [-1] هو الأخير.
  • التقطيع (slice) — [start:end:step] يختار مدى، تمامًا كما في Python: [1:3] هما العنصران 1 و2، و[::-1] يعكس، و[::2] يأخذ واحدًا من كل اثنين.
  • المرشِّح — [?<تعبير>] يُبقي فقط العناصر أو الأعضاء التي يكون التعبير لها صحيحًا (يُشرَح أدناه).

يمكن للقوس أن يحتوي عدة مُحدِّدات معًا: [0, 2, "title"] يختار ثلاثة أشياء في مقطع واحد. ويمكن أن يكون المقطع مقطعًا ابنًا (نقطة أو قوس مفرد) أو مقطع أحفاد (..)، يبحث في العقدة وكل أحفادها — $..author يجد كل author في أي مكان في المستند.

المرشِّحات، والطرق الأربع لمقارنة اللا-شيء

يختبر مُحدِّد المرشِّح كل عنصر بتعبير منطقي، فيه @ يشير إلى العنصر الحالي و$ إلى المستند كله. يمكن للتعبير أن يقارن القيم (==, !=, <, <=, >, >=)، وأن يجمع الاختبارات بـ && و|| و!، وأن يختبر الوجود ببساطة: $..book[[email protected]] يُبقي الكتب التي لها ISBN، لأن @.isbn يختار عقدة فقط حين يكون المفتاح موجودًا.

الجزء الدقيق هو مقارنة شيء غير موجود. استعلام على يسار أو يمين المقارنة يُنتج قيمة أو لا-شيء. تعرّف الـ RFC ذلك بدقة: اللا-شيء يساوي اللا-شيء، واللا-شيء لا يساوي أي قيمة حقيقية، وأي اختبار ترتيب (<, >) ضد اللا-شيء هو ببساطة خطأ. فـ @.price < 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 تطابق . أي حرف عدا إرجاع العربة أو تغذية السطر، ما يعني أنها تطابق فعلًا فاصلَي سطر Unicode U+2028 و U+2029 اللذين تستبعدهما نقطة JavaScript. يترجم هذا المختبِر I-Regexp بأمانة، فيتصرّف النمط هنا كما يقيّمه خادم متوافق.

الفرق بين الدالتين هو التثبيت فقط: match تشترط أن تطابق السلسلة كلها، بينما search تبحث عن النمط في أي مكان داخلها. match(@, "a.*") يقبل "abc"؛ و search(@, "b") يقبل أي سلسلة تحتوي b.

المسارات المُطبَّعة، والتشغيل في المتصفح

لكل تطابق، يعرض هذا المختبِر Normalized Path — الموضع الكنسي الذي تعرّفه الـ RFC، مكتوبًا بصيغة الأقواس وعلامات الاقتباس: $['store']['book'][0]['author']. بخلاف الاستعلام الذي قد يختار عُقَدًا كثيرة، يشير المسار المُطبَّع إلى واحدة بالضبط، مستخدمًا مُحدِّدات الاسم والفهرس فقط ونمط اقتباس ثابت. إنه الجواب عن "من أين جاء هذا التطابق"، وهو ما يتيح تحويل نتيجة البدل إلى مجموعة مواضع محددة. معظم المختبِرات تعرض القيم وتترك لك استنتاج المسارات؛ هذا يعرض الاثنين.

المحرك كله يعمل في متصفحك — يُحلَّل JSON ويُقيَّم الاستعلام على جهازك، ولا شيء مما تلصقه يُرفَع أو يُخزَّن أو يُسجَّل. وهو مُتحقَّق منه مقابل حزمة اختبارات التوافق الرسمية لـ JSONPath، فتتّفق إجاباته مع المعيار لا مع تأويل مكتبة واحدة له.

الأسئلة الشائعة

أي لهجة JSONPath يستخدم هذا؟
RFC 9535، معيار IETF لعام 2024، وهو مُتحقَّق منه مقابل حزمة اختبارات التوافق الرسمية لـ JSONPath. لا يقبل عمدًا أعراف Goessner القديمة حيث تختلف عن الـ RFC؛ يُرفَض مثل هذا الاستعلام مع موضع المشكلة كي تصلحه.
لماذا رُفض استعلامي وهو يعمل في أداة أخرى؟
لأن تلك الأداة تتبع أعراف Goessner السابقة للمعيار، المختلفة عن RFC 9535 في عدة مواضع — $.. وحده، وتعبيرات script مثل [(@.length-1)]، وفهارس بصفر بادئ، وأسماء غير مقتبَسة بأحرف خاصة كلها غير معيارية. استبدلها الـ RFC بمكافئات محددة جيدًا، وهذا المختبِر يلتزم بالـ RFC.
ما هو المسار المُطبَّع؟
الموضع الكنسي لعقدة واحدة، مكتوبًا بصيغة الأقواس وعلامات الاقتباس التي تعرّفها الـ RFC، مثل $['store']['book'][0]['author']. قد يطابق استعلام عُقَدًا كثيرة؛ ولكل تطابق مسار مُطبَّع واحد بالضبط، ولهذا يعرضه المختبِر بجانب كل قيمة.
كيف يتعامل المرشِّح مع قيمة غائبة؟
كلا-شيء، بقواعد تثبّتها الـ RFC: اللا-شيء يساوي اللا-شيء، واللا-شيء لا يساوي أي قيمة حقيقية، وأي مقارنة ترتيب (<, >) تتضمّن اللا-شيء خطأ. فـ @.price < 10 يتخطّى ببساطة عنصرًا بلا price بدل أن يطلق خطأ.
هل تستخدم match() و search() تعبيرات JavaScript النمطية؟
لا. تستخدمان I-Regexp (RFC 9485)، مجموعة فرعية قابلة للنقل. الفرق العملي الرئيسي هو النقطة، التي تطابق كل شيء عدا إرجاع العربة وتغذية السطر — بما في ذلك U+2028 و U+2029 اللذين تستبعدهما نقطة JavaScript. يترجم هذا المختبِر I-Regexp بأمانة كي تطابق النتائج تنفيذًا متوافقًا.
ما الفرق بين match و search؟
التثبيت. match() تشترط أن تطابق السلسلة كلها النمط، كأنها محاطة بمرابط؛ و search() تنجح إن وُجد النمط في أي مكان في السلسلة. كل ما عدا ذلك متطابق.
هل يُرسَل JSON الخاص بي إلى خادم؟
لا. يُحلَّل المستند ويُقيَّم الاستعلام بالكامل في متصفحك، ولا شيء مما تلصقه يُرفَع أو يُسجَّل. آمن للاختبار مقابل استجابة واجهة برمجية حقيقية أو ملف إعدادات.