JSON Toolkit

Фільтри JSONPath поглиблено: рекурсія, зрізи й зіставлення кількох умов

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

Середній~10 хв читанняОпубліковано 2026-10-31Оновлено 2026-10-31
Обкладинка статті «Фільтри 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

Три речі для розуміння:

  1. $..book знаходить кожен вузол із назвою book, незалежно від його глибини;
  2. Обхід відбувається в глибину, а порядок виводу залежить від того, як реалізація відвідує дітей;
  3. .. не може використовувати жодного індексу, тож його ціна масштабується із загальною кількістю вузлів.

У результаті $..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 })

Скриптові вирази створюють три проблеми:

  1. Переносність падає до нуля. Зміна бібліотеки означає переписати кожен вираз;
  2. Межа безпеки зникає. Коли вирази походять від користувачів, обчислення скрипта передає виконання коду;
  3. Статична оптимізація стає неможливою. Реалізація може лише інтерпретувати вузол за вузлом, що повільно й важко кешувати.

Якщо вам справді потрібне порівняння між вузлами, передайте поріг як параметр запиту й фільтруйте вдруге в коді застосунку. Зазвичай це стабільніше, ніж залежати від синтаксису розширення.

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 вузлів робіть це насамперед.