Язык запросов 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 поддерживают четыре вида конструкций:
Логика: && || ! например ?@.price < 10 && @.tags
Сравнение: == != < <= > >=
Существование: голый путь например ?@.tags (проверяет существование)
Функции: 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() и прогоняйте официальный набор соответствия перед сменой реализаций.