Мова запитів JSONPath на практиці
Головне: У 2024 році JSONPath нарешті отримав формальний стандарт, RFC 9535. Ця стаття пояснює синтаксис, семантику фільтрів, функції й історичні відмінності між реалізаціями.

Коротко: спершу локалізуйте явним шляхом, а фільтр накладайте поверх, і пишіть вирази за RFC 9535; використовуйте доступ до властивості або JSON Pointer для одного фіксованого поля, а jq чи код — для агрегації.
Ви, мабуть, бачили вирази на кшталт
$.store.book[?(@.price < 10)].title, але міжнародним стандартом вони стали лише 2024 року.
1. Від де-факто стандарту до формального стандарту
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 підтримують чотири види конструкцій:
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
Кілька практичних прикладів:
// Ціна між 8 і 10
$.store.book[?@.price >= 8 && @.price <= 10]
// Назва містить "JSON" (збіг регулярного виразу; другий аргумент — I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// Більше одного тегу
$.store.book[?length(@.tags) > 1]
// Отримати перше знайдене значення замість списку (value() перетворює одноелементний набір на значення)
$.store.book[?@.price < 9].title.value()
match() — це збіг усього рядка, а search() — пошук підрядка; базовий діалект регулярних виразів — I-Regexp (підмножина RFC 9485),
яка не підтримує ні зворотних посилань, ні lookaround — це водночас обмеження й зручний щит проти 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або кешування) замість повторного розбору на кожен запит; - Для документів із понад 100 000 вузлів розгляньте спершу точне вилучення через JSON Pointer або попередню обробку на сервері через
jq.
Безпека
- Вирази з користувацького введення мають мати обмеження довжини й глибини вкладеності;
- Не дозволяйте JSONPath керувати авторизацією — він може лише читати й не може виразити «які поля невидимі»;
- Коли регулярний вираз, що використовується в
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() і запускайте офіційний набір тестів відповідності перед зміною реалізації.