Фільтри JSONPath поглиблено: рекурсія, зрізи й зіставлення кількох умов
Головне: Рекурсивний спуск, зрізи й фільтри з кількома умовами — саме там JSONPath стає потужним і неправильно використовуваним. Охоплює семантику фільтрів, форми результатів і відмінності бібліотек.

Коротко: спершу локалізуйте відому глибину, потім додайте зрізи й фільтри замість того, щоб тягнутися до $..; голий шлях у фільтрі лише перевіряє існування, тож перевіряйте булеве значення через @.stock == true.
$.store.book[0].titleможе написати кожен. Що насправді ламає JSONPath — це поєднання$..зі зрізами й кількома умовами, коли ви отримуєте забагато результатів, замало результатів або порядок, якого не очікували.
1. Зразковий документ
{
"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"
}
Кожен вираз нижче використовує цей документ як вхід. Тримайте в голові дві передумови: результат — набір вузлів, і його порядок слідує порядку документа, а не порядку написання умов. Усередині фільтра @ вказує лише на поточний вузол-кандидат.
2. Рекурсивний спуск $..: зручний, але дорогий
.. означає «шукати рекурсивно на будь-якій глибині від поточної позиції». RFC 9535 визначає його як сегмент-нащадок, і він може поєднуватися з назвами, шаблонами, індексами, зрізами й фільтрами:
$..price // кожен член із назвою price на будь-якій глибині
$..book[?(@.price < 10)] // будь-яка книжка на будь-якій глибині, потім фільтр
$..[?(@.stock)] // будь-який вузол на будь-якій глибині, де існує stock
Три речі для розуміння:
$..bookзнаходить кожен вузол із назвоюbook, незалежно від його глибини;- Обхід відбувається в глибину, а порядок виводу залежить від того, як реалізація відвідує дітей;
..не може використовувати жодного індексу, тож його ціна масштабується із загальною кількістю вузлів.
У результаті $..price і $.store.book[*].price можуть різнитися лише ціною велосипеда у виводі, але різнитися на порядок у ціні. Коли знаєте глибину, завжди пишіть явний шлях. Якщо хочете лише порівняти межі двох мов шляхів, дивіться JSONPath проти JSON Pointer.
3. Зрізи масивів і шаблони: три пастки
Синтаксис зрізу — [start:end:step], включно з початком і виключно з кінцем, відʼємні числа рахуються з хвоста:
| Вираз | Значення | Результат (із чотирма книжками) |
|---|---|---|
[0:2] |
Перші дві книжки | Книжки 1 і 2 |
[1:] |
Від другої книжки до кінця | Книжки 2, 3 і 4 |
[:2] |
Перші дві книжки | Книжки 1 і 2 |
[-2:] |
Останні дві книжки | Книжки 3 і 4 |
[::2] |
Кожна друга книжка | Книжки 1 і 3 |
[::-1] |
Зворотний порядок | Книжки 4, 3, 2 і 1 |
Три пастки:
- Крок нуль невалідний. RFC 9535 вимагає ненульовий крок, тоді як деякі реалізації мовчки повертають порожню множину;
- Відʼємні кроки підтримуються не всюди. Оригінальна реалізація Гьосснера їх підтримує, але деякі мінімальні реалізації приймають лише додатні кроки;
- Фільтрація після зрізу не розширює область.
[0:2][?(@.stock)]фільтрує лише дві книжки, отримані зрізом; воно не повертається до всього масиву.
Шаблон [*] може поєднуватися зі зрізами, але [*] дає «всі дочірні вузли». Якщо ці діти — обʼєкти, додавання .title дає всі назви. Якщо дитина сама є масивом, .title не вирівняє неявно ще один рівень. Пишіть [*] знову, коли треба розгорнути; не очікуйте неявного вирівнювання.
4. Фільтри з кількома умовами: порівняння, логіка й існування
Фільтр [?(...)] може містити чотири види конструкцій: порівняння, логічні оператори, перевірки існування й функції. Типовий запит із кількома умовами виглядає так:
$..book[?(@.price < 10 && @.stock)]
Він означає «ціна нижче 10 і stock істинний». Зверніть увагу на кілька семантичних деталей:
@.stock— це перевірка існування, а не «значення істинне». Якщо поле існує, але його значенняfalse, умова все одно виконується. Щоб перевірити булеве значення, треба писати@.stock == true;&&звʼязує тісніше за||. Додавайте дужки, змішуючи їх, замість покладатися на памʼять;- Обидві сторони порівняння мають мати той самий тип. Числа й рядки не перетворюються неявно;
@.price < "10"є хибним або помилкою в строгих реалізаціях; - Відсутнє поле завжди робить порівняння хибним.
@.missing < 10не спричиняє помилки; воно просто нічого не зіставляє.
Це найлегше місце спіткнутися, пишучи фільтри так, ніби це JavaScript: if (obj.stock) перевіряє істинність у JavaScript, тоді як JSONPath перевіряє існування. Про повний стандартний синтаксис і функції поверніться до Мова запитів JSONPath на практиці.
5. Скриптові вирази: більше сили, менше переносності
Оригінальна реалізація Гьосснера й jsonpath-plus дозволяють порівняння між вузлами на кшталт [?(@.price < $.max)] і навіть довільні скрипти. RFC 9535 прямо дозволяє лише доступ до поточного вузла @ і забороняє $ усередині фільтра.
// Працює в старіших реалізаціях; не дозволено RFC 9535
$..book[?(@.price < $.expensive)]
// Стандартний підхід: передайте поріг від викликача
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
Скриптові вирази створюють три проблеми:
- Переносність падає до нуля. Зміна бібліотеки означає переписати кожен вираз;
- Межа безпеки зникає. Коли вирази походять від користувачів, обчислення скрипта передає виконання коду;
- Статична оптимізація стає неможливою. Реалізація може лише інтерпретувати вузол за вузлом, що повільно й важко кешувати.
Якщо вам справді потрібне порівняння між вузлами, передайте поріг як параметр запиту й фільтруйте вдруге в коді застосунку. Зазвичай це стабільніше, ніж залежати від синтаксису розширення.
6. Форми результатів: ви думаєте, що отримуєте значення, але отримуєте вузли
Той самий вираз може повернути принаймні три форми в різних бібліотеках:
["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // масив значень
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // шлях + значення
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + значення
Це безпосередньо впливає на три речі: як фронтенд читає результати, як результати дедуплікуються й як результати запиту стають операціями запису. Nodelist, визначений RFC 9535, — це набір посилань на вузли, але реалізації JavaScript зазвичай повертають лише значення, бо посилання не можуть перетинати межі процесу.
Практична порада:
- Завжди читайте одне значення через
[0]абоvalue(); ніколи не припускайте, що результат — скаляр; - Коли треба записати назад, оберіть реалізацію, що повертає JSON Pointers, або збудуйте покажчик самі;
- Порожній результат і результат
[null]різні; перевіряйте довжину, а не істинність.
7. Відмінності бібліотек: запустіть набір відповідності перед міграцією
| Можливість | Оригінал Гьосснера | jsonpath-plus | Строгий RFC 9535 |
|---|---|---|---|
Дужки фільтрів ?() |
Обовʼязкові | Сумісні | Необовʼязкові |
Посилання поза коренем $.x |
Підтримується | Підтримується | Заборонено |
| Скриптові вирази | Підтримуються | Підтримуються | Заборонені |
| Відʼємні індекси й кроки | Підтримуються | Підтримуються | Підтримуються |
| Повертати шляхи | Налаштовується | Налаштовується | Не специфіковано |
Функції на кшталт length() |
Немає | Розширення | Стандартні функції |
Перед міграцією перевірте принаймні чотири речі: чи обовʼязкові дужки, чи може $ зʼявлятися всередині фільтра, як поводяться відʼємні кроки й структура результатів. Офіційний набір тестів відповідності покриває більшість синтаксичних відмінностей і запобігає тихим збоям на кшталт «три рядки зникли після зміни бібліотеки».
8. Продуктивність і чек-лист неправильного використання
Продуктивність зводиться до одного речення: JSONPath — це повний обхід, а .. — найдорожчий вид повного обходу.
- Для документів із понад 100 000 вузлів звужте область явним шляхом, потім фільтруйте підмножину;
- Компілюйте вираз один раз і повторно використовуйте; не розбирайте рядок заново в циклі;
- Переходьте на
jqабо код для агрегації (підсумовування, групування, сортування); JSONPath лише вибирає; - Ставте умови якомога раніше:
$.store.book[?(@.price < 10)]сканує значно менше нерелевантних вузлів, ніж$..[?(@.price < 10)].
Чек-лист неправильного використання:
- [ ] Трактування перевірки існування як перевірки булевої істинності;
- [ ] Використання
$..замість відомої глибини; - [ ] Залежність від скриптових виразів для порівняння між вузлами;
- [ ] Припущення, що результат завжди масив значень;
- [ ] Дозвіл контрольованих користувачем виразів без обмежень довжини й вкладеності.
Часті питання
Чому @.stock усе одно зіставляється, коли поле false?
Бо голий шлях усередині фільтра означає «поле існує», а не «значення істинне». Щоб перевірити булеве значення, порівняйте явно з @.stock == true. Це найбільша семантична різниця між JSONPath і істинністю JavaScript.
Чи стабільний порядок рекурсивного спуску?
Ні. RFC 9535 визначає семантику набору вузлів, а порядок обходу залежить від того, як реалізація відвідує дітей. Коли потрібен фіксований порядок, локалізуйте масив і читайте за індексом або сортуйте результати в коді застосунку.
Чи можна поєднувати зрізи й фільтри?
Так, але памʼятайте область. $.store.book[0:2][?(@.stock)] фільтрує лише дві книжки, отримані зрізом. Щоб фільтрувати спершу, а різати потім, пишіть $.store.book[?(@.stock)][0:2]; інший порядок дає інший результат.
Чому jsonpath-plus підтримує $.expensive, коли стандарт це забороняє?
Бо фільтри визначені лише через поточний вузол @. Посилання поза коренем робить вираз залежним від усього контексту документа, що важко аналізувати статично й переносити між мовами. Передавайте поріг параметром замість вбудовувати його у вираз.
Як не дати рекурсивному спуску сканувати нерелевантні вузли?
Пишіть явний шлях, коли знаєте глибину, спершу звужте область і фільтруйте лише підмножину, замість використовувати $.. на місці відомого рівня. Ставте умови якомога раніше: $.store.book[?(@.price < 10)] сканує значно менше нерелевантних вузлів, ніж $..[?(@.price < 10)]. Для документів із понад 100 000 вузлів робіть це насамперед.