A linguagem de consulta JSONPath na prática
Resumo: Em 2024 o JSONPath finalmente ganhou um padrão formal, a RFC 9535. Este artigo explica a sintaxe, a semântica de filtros, as funções e as diferenças históricas entre implementações.

Resumo da ópera: localize primeiro com um caminho explícito e filtre em cima dele, e escreva expressões contra a RFC 9535; use acesso a propriedade ou JSON Pointer para um único campo fixo, e jq ou código para agregação.
Você provavelmente já viu expressões como
$.store.book[?(@.price < 10)].title, mas ela só se tornou padrão internacional em 2024.
1. De padrão de fato a padrão formal
O JSONPath foi proposto por Stefan Gössner em 2007, e por muito tempo a única "especificação" era um único post de blog, com cada implementação de linguagem fazendo do seu jeito:
- se filtros precisam de parênteses (
?(@.a)vs?@.a); - se o índice de array
[-1]é suportado; - se o resultado retorna "valores" ou "valores e caminhos";
- se expressões de script (
?(@.price < $.max)) são permitidas.
A RFC 9535, publicada em fevereiro de 2024, convergiu essas divergências em uma única sintaxe e semântica e forneceu uma suíte oficial de testes de conformidade. Projetos novos devem seguir a RFC 9535; ao migrar projetos antigos, confirme primeiro a versão da implementação.
2. Folha de referência de sintaxe
| Sintaxe | Significado | Exemplo |
|---|---|---|
$ |
Nó raiz | $.store |
@ |
Nó atual dentro de um filtro | $[?@.price < 10] |
.name |
Nó filho (o nome precisa corresponder a [A-Za-z_][A-Za-z0-9_]*) |
$.store.book |
['name'] |
Nó filho (qualquer nome, incluindo não-ASCII e pontos) | $['store']['书'] |
[0] |
Índice de array; números negativos contam a partir do fim | $.book[0], $.book[-1] |
[0,2] |
Múltiplos índices / múltiplos nomes | $.book[0,2] |
[1:3] |
Fatia, início inclusivo e fim exclusivo | $.book[1:3] |
[::2] |
Fatia com passo | $.book[::2] |
[*] |
Wildcard para todos os nós filhos | $.book[*].title |
..name |
Descida recursiva, buscando em qualquer profundidade | $..price |
..[*] |
Recursivamente pegar todos os nós | $..* |
[?<expr>] |
Seletor de filtro | $.book[?@.price < 10] |
Uma semântica que você precisa lembrar: o JSONPath retorna um conjunto de nós (nodelist), não um valor único. Mesmo quando apenas um nó corresponde, o resultado é uma lista de comprimento um.
3. Percorrendo a sintaxe com uma amostra
{
"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
}
| Expressão | Resultado |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
4 preços (incluindo o preço da bicicleta) |
$.store.book[*].title |
3 títulos |
$.store.book[?@.price < 10].title |
Títulos dos livros 1 e 3 |
$.store.book[?@.tags] |
Todos os livros que têm tags (teste de existência) |
$.store.book[-1].title |
Título do último livro |
$..book[0,1] |
Os dois primeiros livros |
$.store.book[?@.price < $.expensive] |
Sintaticamente dependente da implementação; a RFC 9535 não permite referências cruzadas à raiz |
A última linha é uma armadilha histórica: muitas implementações permitem $.expensive dentro de um filtro para comparação entre nós, o que a RFC 9535 proíbe explicitamente —
um filtro só consegue acessar o nó atual @. Quando precisar comparar com um limiar global, passe o limiar como argumento para a função de consulta.
4. O que os filtros conseguem expressar
As expressões de filtro da RFC 9535 suportam quatro tipos de construções:
Lógica: && || ! ex.: ?@.price < 10 && @.tags
Comparação: == != < <= > >=
Existência: um caminho cru ex.: ?@.tags (testa existência)
Funções: length() count() match() search() value()
Alguns exemplos práticos:
// Preço entre 8 e 10
$.store.book[?@.price >= 8 && @.price <= 10]
// Título contém "JSON" (correspondência de regex; o segundo argumento é um I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// Mais de uma tag
$.store.book[?length(@.tags) > 1]
// Pegar o primeiro valor correspondente em vez de uma lista (value() transforma um conjunto de nó único em um valor)
$.store.book[?@.price < 9].title.value()
match() é uma correspondência de string inteira e search() é uma busca de substring; o dialeto de regex subjacente é o I-Regexp (um subconjunto da RFC 9485),
que não suporta backreferences nem lookaround — isso é tanto uma limitação quanto um escudo útil contra DoS de regex.
5. Diferenças entre implementações: verifique antes de migrar
| Implementação | Linguagem | Suporte à RFC 9535 | Observações |
|---|---|---|---|
jsonpath-plus |
JavaScript | Majoritariamente compatível | Suporta funções de extensão e expressões de script; mais capaz mas não estritamente padrão |
jsonpath (dchester) |
JavaScript | Parcial | Biblioteca antiga com sintaxe ?() e estrutura de resultado diferente |
ojg / gjson |
Go | ojg tem bom suporte |
gjson usa outra sintaxe de caminho e não é JSONPath |
jsonpath-ng |
Python | Parcial | Suporta extensões e é razoavelmente permissiva |
Jayway JsonPath |
Java | Parcial | Ecossistema amplo; comportamento histórico difere da RFC |
Três verificações antes de migrar:
- Se parênteses de filtro são obrigatórios;
- Se índices negativos como
-1são suportados; - Se o resultado é um "array de valores" ou um "array de objetos
{path, value}".
A terceira é a mais fácil de tropeçar: o frontend lê result[0], e após trocar de biblioteca recebe um objeto, deixando a página em branco.
6. Desempenho e segurança
Desempenho
- O JSONPath é essencialmente uma única travessia com complexidade O(número de nós), e não consegue usar índices;
- A descida recursiva com
..é a mais cara; se você consegue localizar com$.store.bookprimeiro e depois filtrar, não escreva$..book; - Compile expressões uma vez e reutilize-as (a maioria das bibliotecas oferece
compileou cache) em vez de reanalisar a cada consulta; - Para documentos com mais de 100.000 nós, considere a extração exata com JSON Pointer primeiro, ou pré-processamento no servidor com
jq.
Segurança
- Expressões vindas da entrada do usuário precisam de limites de comprimento e profundidade de aninhamento;
- Não deixe o JSONPath cuidar de autorização — ele só consegue ler, e não consegue expressar "quais campos são invisíveis";
- Quando a regex usada por
match()vem dos usuários, limite sua complexidade; embora seja mais segura que PCRE, um limite de comprimento ainda é recomendado.
7. Quando não usar JSONPath
- Você só precisa de um campo fixo → use
data.store.bicycle.colordiretamente; - Você precisa gravar de volta após modificar → use JSON Pointer ou um patch; o JSONPath não grava de volta;
- Você precisa de agregação (somar, agrupar) → use
jq, SQL ou código; o JSONPath apenas seleciona; - O documento é enorme e você precisa apenas de alguns campos → use um analisador de streaming em vez de ler a árvore inteira na memória primeiro.
8. Lista de verificação
- [ ] As expressões seguem a RFC 9535 e não dependem de referências cruzadas à raiz;
- [ ] Está claro que o resultado é um conjunto de nós; use
[0]ouvalue()para obter um valor único; - [ ] As expressões são pré-compiladas e reutilizadas;
- [ ] Expressões controladas pelo usuário têm limites de comprimento e complexidade;
- [ ] Os casos oficiais de conformidade são executados ao trocar de implementação.
Perguntas frequentes
Como devo escolher entre JSON Pointer e JSONPath?
Use acesso simples a propriedade ou um JSON Pointer quando só precisar de um campo fixo; recorra ao JSONPath quando precisar de filtros, wildcards ou descida recursiva. O JSONPath só seleciona e nunca grava de volta, então a gravação de volta pertence ao JSON Pointer ou a um patch.
Por que um filtro não consegue referenciar $.expensive?
A RFC 9535 proíbe explicitamente referências cruzadas à raiz; um filtro só pode acessar o nó atual @. Passe o limiar como argumento para a função de consulta. Implementações mais antigas aceitam a sintaxe, mas ela quebra quando você troca ou atualiza bibliotecas.
Devo confiar em result[0] para ler um valor?
Só depois de confirmar que a implementação atual retorna um array de valores. Algumas bibliotecas retornam objetos {path, value}, então result[0] vira um objeto e a página fica em branco. Leia valores únicos por [0] ou value(), e rode a suíte oficial de conformidade antes de trocar de implementação.