فیلترهای JSONPath بهطور عمیق: بازگشت، برشها و تطبیق چندشرطی
جمعبندی: نزول بازگشتی، برشها و فیلترهای چندشرطی جایی هستند که JSONPath هم قدرتمند میشود و هم بد استفاده. معناشناسی فیلتر، شکل نتیجه و تفاوت کتابخانهها را پوشش میدهد.

جمعبندی: اول عمق شناختهشده را مکانیابی کنید، سپس برش و فیلتر اضافه کنید نه اینکه به $.. بروید؛ مسیر خالی در فیلتر فقط وجود را میآزماید، پس بولی را با @.stock == true بیازمایید.
هر کسی میتواند
$.store.book[0].titleبنویسد. آنچه واقعاً JSONPath را میشکند ترکیب$..با برشها و چند شرط است و بعد نتیجه بیش از حد، کم یا با ترتیبی غیرمنتظره میگیرید.
۱. یک سند نمونه
{
"store": {
"book": [
{ "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
{ "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
{ "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
{ "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"updatedAt": "2026-10-31"
}
هر عبارت زیر این سند را بهعنوان ورودی استفاده میکند. دو فرض را در ذهن نگه دارید: نتیجه یک مجموعه گره است و ترتیبش از ترتیب سند پیروی میکند نه ترتیب نوشتن شرطها. داخل فیلتر، @ فقط به گره کاندید جاری اشاره دارد.
۲. نزول بازگشتی $..: راحت اما گران
.. یعنی «از موقعیت جاری در هر عمقی بازگشتی جستوجو کن». RFC 9535 آن را قطعه نزولی تعریف میکند و میتواند با نامها، نویسههای عمومی، اندیسها، برشها و فیلترها ترکیب شود:
$..price // every member named price at any depth
$..book[?(@.price < 10)] // any book at any depth, then filter
$..[?(@.stock)] // any node at any depth where stock exists
سه چیز برای فهمیدن:
$..bookهر گرهای به نامbookرا پیدا میکند، بدون توجه به عمقش؛- پیمایش عمق-اول است و ترتیب خروجی به نحوه بازدید پیادهسازی از فرزندان بستگی دارد؛
..نمیتواند از هیچ ایندکسی استفاده کند، پس هزینهاش با تعداد کل گره مقیاس میگیرد.
در نتیجه، $..price و $.store.book[*].price ممکن است فقط در قیمت دوچرخه خروجیشان تفاوت داشته باشند، اما یک مرتبه بزرگی در هزینه تفاوت دارند. وقتی عمق را میدانید، همیشه مسیر صریح بنویسید. اگر فقط میخواهید مرزهای دو زبان مسیر را مقایسه کنید، JSONPath در برابر JSON Pointer را ببینید.
۳. برشها و نویسههای عمومی آرایه: سه تله
نحو برش [start:end:step] است، شامل شروع و انحصاری پایان، با اعداد منفی که از انتها میشمارند:
| عبارت | معنا | نتیجه (با چهار کتاب) |
|---|---|---|
[0:2] |
دو کتاب اول | کتابهای ۱ و ۲ |
[1:] |
از کتاب دوم تا انتها | کتابهای ۲، ۳ و ۴ |
[:2] |
دو کتاب اول | کتابهای ۱ و ۲ |
[-2:] |
دو کتاب آخر | کتابهای ۳ و ۴ |
[::2] |
یک کتاب در میان | کتابهای ۱ و ۳ |
[::-1] |
ترتیب معکوس | کتابهای ۴، ۳، ۲ و ۱ |
سه تله:
- گام صفر نامعتبر است. RFC 9535 گام غیرصفر میخواهد، در حالی که برخی پیادهسازیها بیصدا مجموعه خالی برمیگردانند؛
- گام منفی همهجا پشتیبانی نمیشود. پیادهسازی اصلی Goessner پشتیبانی میکند، اما برخی پیادهسازیهای حداقلی فقط گام مثبت میپذیرند؛
- فیلتر پس از برش دامنه را گسترش نمیدهد.
[0:2][?(@.stock)]فقط دو کتاب تولیدشده توسط برش را فیلتر میکند؛ به کل آرایه برنمیگردد.
نویسه عمومی [*] میتواند با برش ترکیب شود، اما [*] «همه گرههای فرزند» میدهد. اگر آن فرزندان شیء باشند، افزودن .title همه عنوانها را میدهد. اگر فرزندی خودش آرایه باشد، .title خودکار یک سطح دیگر را تخت نمیکند. وقتی باید گسترش دهید [*] را دوباره بنویسید؛ انتظار تختسازی ضمنی نداشته باشید.
۴. فیلترهای چندشرطی: مقایسه، منطق و وجود
فیلتر [?(...)] میتواند چهار نوع ساختار داشته باشد: مقایسهها، عملگرهای منطقی، آزمونهای وجود و توابع. یک پرسوجوی چندشرطی معمول اینگونه است:
$..book[?(@.price < 10 && @.stock)]
یعنی «قیمت زیر ۱۰ و stock درستنما». به چند جزئیات معنایی توجه کنید:
@.stockآزمون وجود است، نه «مقدار true است». اگر فیلد وجود دارد اما مقدارشfalseاست، شرط همچنان برقرار است. برای آزمودن مقدار بولی باید@.stock == trueبنویسید؛&&محکمتر از||بسته میشود. هنگام ترکیب پرانتز بگذارید نه اینکه به حافظه تکیه کنید؛- هر دو طرف مقایسه باید نوع یکسان داشته باشند. عدد و رشته ضمنی تبدیل نمیشوند؛
@.price < "10"در پیادهسازیهای سختگیر false یا خطاست؛ - فیلد گمشده همیشه مقایسه را false میکند.
@.missing < 10خطا نمیدهد؛ صرفاً چیزی تطبیق نمیدهد.
این آسانترین جای زمین خوردن هنگام نوشتن فیلتر به سبک جاوااسکریپت است: if (obj.stock) در جاوااسکریپت درستنمایی را میآزماید، در حالی که JSONPath وجود را. برای نحو و توابع کامل استاندارد، زبان پرسوجوی JSONPath در عمل را دوباره ببینید.
۵. عبارتهای اسکریپتی: قدرت بیشتر، قابلیت انتقال کمتر
پیادهسازی اصلی Goessner و jsonpath-plus مقایسه بینگرهی مانند [?(@.price < $.max)] و حتی اسکریپت دلخواه را مجاز میدانند. RFC 9535 صراحتاً فقط دسترسی به گره جاری @ را مجاز میداند و $ را داخل فیلتر ممنوع میکند.
// Works in older implementations; not allowed by RFC 9535
$..book[?(@.price < $.expensive)]
// Standard approach: pass the threshold from the caller
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
عبارتهای اسکریپتی سه مشکل میسازند:
- قابلیت انتقال به صفر میرسد. تعویض کتابخانه یعنی بازنویسی هر عبارت؛
- مرز امنیتی ناپدید میشود. وقتی عبارتها از کاربران میآیند، ارزیابی اسکریپت اجرای کد را تسلیم میکند؛
- بهینهسازی ایستا ناممکن میشود. پیادهسازی فقط میتواند گرهبهگره تفسیر کند، که کند و سختکش است.
اگر واقعاً به مقایسه بینگرهی نیاز دارید، آستانه را پارامتر پرسوجو بدهید و در کد برنامه دوباره فیلتر کنید. این معمولاً از وابستگی به نحو افزونه پایدارتر است.
۶. شکل نتیجه: فکر میکنید مقدار میگیرید، اما گره میگیرید
همان عبارت میتواند دستکم سه شکل بین کتابخانهها برگرداند:
["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // value array
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // path + value
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + value
این مستقیماً سه چیز را تحت تأثیر قرار میدهد: نحوه خواندن نتایج توسط فرانتاند، نحوه یکتا کردن نتایج و تبدیل نتایج پرسوجو به عملیات نوشتن. nodelist تعریفشده در RFC 9535 مجموعهای از ارجاعهای گره است، اما پیادهسازیهای جاوااسکریپت معمولاً فقط مقدارها برمیگردانند چون ارجاعها نمیتوانند از مرز فرایند عبور کنند.
توصیه عملی:
- همیشه مقدار واحد را از
[0]یاvalue()بخوانید؛ هرگز فرض نکنید نتیجه اسکالر است؛ - وقتی باید بنویسید، پیادهسازیای انتخاب کنید که JSON Pointer برمیگرداند یا pointer را خودتان بسازید؛
- نتیجه خالی و نتیجه
[null]متفاوتاند؛ طول را بررسی کنید نه درستنمایی.
۷. تفاوت کتابخانهها: مجموعه انطباق را پیش از مهاجرت اجرا کنید
| توانایی | Goessner اصلی | jsonpath-plus | RFC 9535 سختگیر |
|---|---|---|---|
پرانتز فیلتر ?() |
الزامی | سازگار | اختیاری |
ارجاع بینریشهای $.x |
پشتیبانی | پشتیبانی | ممنوع |
| عبارتهای اسکریپتی | پشتیبانی | پشتیبانی | ممنوع |
| اندیس و گام منفی | پشتیبانی | پشتیبانی | پشتیبانی |
| برگرداندن مسیرها | قابل پیکربندی | قابل پیکربندی | تعریفنشده |
توابعی مانند length() |
ندارد | افزونهها | توابع استاندارد |
پیش از مهاجرت دستکم چهار چیز را تأیید کنید: آیا پرانتز الزامی است، آیا $ میتواند داخل فیلتر ظاهر شود، گام منفی چگونه رفتار میکند، و ساختار نتیجه. مجموعه آزمون انطباق رسمی بیشتر تفاوتهای نحو را پوشش میدهد و از شکستهای بیصدایی مانند «سه ردیف پس از تعویض کتابخانه ناپدید شد» جلوگیری میکند.
۸. چکلیست عملکرد و سوءاستفاده
عملکرد به یک جمله برمیگردد: JSONPath یک پیمایش کامل است، و .. گرانترین نوع پیمایش کامل.
- برای اسناد با بیش از ۱۰۰,۰۰۰ گره، اول با مسیر صریح دامنه را باریک کنید، سپس زیرمجموعه را فیلتر کنید؛
- عبارت را یک بار کامپایل و بازاستفاده کنید؛ رشته را داخل حلقه دوباره تجزیه نکنید؛
- برای تجمیع (جمع، گروهبندی، مرتبسازی) به
jqیا کد بروید؛ JSONPath فقط انتخاب میکند؛ - شرطها را هر چه زودتر بگذارید:
$.store.book[?(@.price < 10)]بسیار گرههای نامرتبط کمتری از$..[?(@.price < 10)]پویش میکند.
چکلیست سوءاستفاده:
- [ ] آزمون وجود را بهعنوان آزمون درستنمایی بولی گرفتن؛
- [ ] استفاده از
$..بهجای عمق شناختهشده؛ - [ ] تکیه بر عبارتهای اسکریپتی برای مقایسه بینگرهی؛
- [ ] فرض اینکه نتیجه همیشه آرایهای از مقدارهاست؛
- [ ] اجازه دادن به عبارتهای تحت کنترل کاربر بدون محدودیت طول و تودرتویی.
پرسشهای متداول
چرا @.stock وقتی فیلد false است همچنان تطبیق میشود؟
چون مسیر خالی داخل فیلتر یعنی «فیلد وجود دارد»، نه «مقدار true است». برای آزمودن بولی، صریح با @.stock == true مقایسه کنید. این بزرگترین تفاوت معنایی JSONPath با درستنمایی جاوااسکریپت است.
آیا ترتیب نزول بازگشتی پایدار است؟
نه. RFC 9535 معناشناسی مجموعه گره را مشخص میکند و ترتیب پیمایش به نحوه بازدید پیادهسازی از فرزندان بستگی دارد. وقتی ترتیب ثابت لازم دارید، آرایه را مکانیابی و با اندیس بخوانید، یا نتایج را در کد برنامه مرتب کنید.
آیا برشها و فیلترها میتوانند ترکیب شوند؟
بله، اما دامنه را به یاد داشته باشید. $.store.book[0:2][?(@.stock)] فقط دو کتاب تولیدشده توسط برش را فیلتر میکند. برای فیلتر اول و برش دوم، $.store.book[?(@.stock)][0:2] بنویسید؛ ترتیب متفاوت نتیجه متفاوت میدهد.
چرا jsonpath-plus از $.expensive پشتیبانی میکند وقتی استاندارد ممنوعش میکند؟
چون فیلترها فقط بر اساس گره جاری @ تعریف شدهاند. ارجاع بینریشهای عبارت را به کل بافت سند وابسته میکند، که تحلیل ایستا و انتقال بین زبانها را سخت میکند. بهجایش آستانه را پارامتر بدهید نه اینکه در عبارت جاسازی کنید.
چگونه مانع پویش گرههای نامرتبط توسط نزول بازگشتی شوم؟
هر جا عمق را میدانید مسیر صریح بنویسید، اول دامنه را باریک کنید و فقط زیرمجموعه را فیلتر کنید نه اینکه $.. را جای سطح شناختهشده بگذارید. شرطها را هر چه زودتر بگذارید: $.store.book[?(@.price < 10)] بسیار گرههای نامرتبط کمتری از $..[?(@.price < 10)] پویش میکند. برای اسناد با بیش از ۱۰۰,۰۰۰ گره اول این کار را بکنید.