لغة استعلام JSONPath في التطبيق العملي
الخلاصة: في 2024 حصلت JSONPath أخيرًا على معيار رسمي هو RFC 9535. يشرح هذا المقال الصياغة، ودلالات المرشحات، والدوال، والفروق التاريخية بين التطبيقات.

الخلاصة: حدّد بمسار صريح أولًا ثم رشّح فوقه، واكتب التعبيرات وفق RFC 9535؛ واستخدم الوصول إلى الخصائص أو JSON Pointer لحقل ثابت واحد، وjq أو الشيفرة للتجميع.
ربما رأيت تعبيرات مثل
$.store.book[?(@.price < 10)].title، لكنها لم تصبح معيارًا دوليًا إلا في 2024.
1. من معيار فعلي إلى معيار رسمي
اقترح Stefan Gössner لغة JSONPath في 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 أربعة أنواع من التراكيب:
المنطق: && || ! مثال ?@.price < 10 && @.tags
المقارنة: == != < <= > >=
الوجود: مسار مجرد مثال ?@.tags (يفحص الوجود)
الدوال: length() count() match() search() value()
وفيما يلي أمثلة عملية:
// السعر بين 8 و10
$.store.book[?@.price >= 8 && @.price <= 10]
// العنوان يحتوي "JSON" (مطابقة regex؛ والوسيط الثاني I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// أكثر من وسم واحد
$.store.book[?length(@.tags) > 1]
// الحصول على أول قيمة مطابقة بدلًا من قائمة (value() يحوّل مجموعة عقدة واحدة إلى قيمة)
$.store.book[?@.price < 9].title.value()
match() مطابقة للسلسلة كاملة، وsearch() بحث عن سلسلة فرعية؛ ولهجة regex الأساسية هي I-Regexp (مجموعة فرعية من RFC 9485)،
التي لا تدعم المراجع الخلفية ولا الاستطلاع حول النص — وهذا قيد، وفي الوقت نفسه درع مريح ضد هجمات 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أو التخزين المؤقت) بدلًا من إعادة التحليل في كل استعلام؛ - للمستندات التي تزيد عقدها على 100000، فكّر أولًا في الاستخراج الدقيق بـ JSON Pointer، أو المعالجة المسبقة على الخادم بـ
jq.
الأمان
- يجب أن تكون للتعبيرات الواردة من المستخدم حدود على الطول وعمق التداخل؛
- لا تدع JSONPath تتعامل مع التفويض — فهي تقرأ فقط، ولا تستطيع التعبير عن "أي الحقول غير مرئية"؛
- عندما يأتي regex المستخدم في
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()، وشغّل مجموعة المطابقة الرسمية قبل تبديل التطبيقات.