Filtros JSONPath em profundidade: recursão, fatias e correspondência de múltiplas condições
Resumo: Descida recursiva, fatias e filtros de múltiplas condições são onde o JSONPath fica poderoso e mal usado. Cobre semântica de filtros, formas de resultado e diferenças entre bibliotecas.

Resumo da ópera: localize uma profundidade conhecida primeiro e depois adicione fatias e filtros em vez de recorrer a $..; um caminho cru em um filtro só testa existência, então teste um booleano com @.stock == true.
Qualquer um escreve
$.store.book[0].title. O que realmente quebra o JSONPath é combinar$..com fatias e múltiplas condições e depois obter resultados demais, de menos, ou uma ordem que você não esperava.
1. Um documento de amostra
{
"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"
}
Toda expressão abaixo usa este documento como entrada. Tenha duas premissas em mente: o resultado é um conjunto de nós, e sua ordem segue a ordem do documento em vez da ordem em que as condições foram escritas. Dentro de um filtro, @ se refere apenas ao nó candidato atual.
2. Descida recursiva $..: conveniente mas cara
.. significa "buscar recursivamente em qualquer profundidade a partir da posição atual". A RFC 9535 a define como um segmento descendente, e ela pode ser combinada com nomes, wildcards, índices, fatias e filtros:
$..price // cada membro chamado price em qualquer profundidade
$..book[?(@.price < 10)] // qualquer book em qualquer profundidade, depois filtrar
$..[?(@.stock)] // qualquer nó em qualquer profundidade onde stock existe
Três coisas a entender:
$..bookencontra cada nó chamadobook, independentemente da profundidade;- A travessia é em profundidade, e a ordem de saída depende de como a implementação visita os filhos;
..não consegue usar nenhum índice, então seu custo escala com o número total de nós.
Como resultado, $..price e $.store.book[*].price podem diferir apenas pelo preço da bicicleta na saída, mas diferir por uma ordem de magnitude em custo. Quando você conhece a profundidade, sempre escreva o caminho explícito. Se você só quer comparar as fronteiras das duas linguagens de caminho, veja JSONPath vs JSON Pointer.
3. Fatias de array e wildcards: três armadilhas
A sintaxe de fatia é [start:end:step], inclusiva no início e exclusiva no fim, com números negativos contando a partir da cauda:
| Expressão | Significado | Resultado (com quatro livros) |
|---|---|---|
[0:2] |
Dois primeiros livros | Livros 1 e 2 |
[1:] |
Do segundo livro ao fim | Livros 2, 3 e 4 |
[:2] |
Dois primeiros livros | Livros 1 e 2 |
[-2:] |
Dois últimos livros | Livros 3 e 4 |
[::2] |
Um livro sim, outro não | Livros 1 e 3 |
[::-1] |
Ordem inversa | Livros 4, 3, 2 e 1 |
Três armadilhas:
- Um passo zero é inválido. A RFC 9535 exige um passo diferente de zero, enquanto algumas implementações retornam silenciosamente um conjunto vazio;
- Passos negativos não são universalmente suportados. A implementação original de Goessner os suporta, mas algumas implementações mínimas aceitam apenas passos positivos;
- Filtrar após uma fatia não amplia o escopo.
[0:2][?(@.stock)]só filtra os dois livros produzidos pela fatia; não volta ao array inteiro.
O wildcard [*] pode ser combinado com fatias, mas [*] produz "todos os nós filhos". Se esses filhos são objetos, acrescentar .title dá todos os títulos. Se um filho é ele mesmo um array, .title não vai achatá-lo implicitamente em outro nível. Escreva [*] de novo quando precisar expandir; não espere achatamento implícito.
4. Filtros de múltiplas condições: comparações, lógica e existência
Um filtro [?(...)] pode conter quatro tipos de construção: comparações, operadores lógicos, testes de existência e funções. Uma consulta comum de múltiplas condições fica assim:
$..book[?(@.price < 10 && @.stock)]
Ela significa "preço abaixo de 10 e stock é verdadeiro". Preste atenção a vários detalhes semânticos:
@.stocké um teste de existência, não "o valor é true". Se o campo existe mas seu valor éfalse, a condição ainda vale. Para testar um valor booleano você precisa escrever@.stock == true;&&tem precedência maior que||. Adicione parênteses ao misturá-los em vez de confiar na memória;- Os dois lados de uma comparação precisam ter o mesmo tipo. Números e strings não são convertidos implicitamente;
@.price < "10"é falso ou erro em implementações estritas; - Um campo ausente sempre torna uma comparação falsa.
@.missing < 10não gera erro; simplesmente não corresponde a nada.
Este é o lugar mais fácil de tropeçar ao escrever filtros como se fossem JavaScript: if (obj.stock) testa veracidade em JavaScript, enquanto JSONPath testa existência. Para a sintaxe e as funções completas do padrão, revise A linguagem de consulta JSONPath na prática.
5. Expressões de script: mais poder, menos portabilidade
A implementação original de Goessner e o jsonpath-plus permitem comparações entre nós como [?(@.price < $.max)], e até scripts arbitrários. A RFC 9535 permite explicitamente apenas acesso ao nó atual @ e proíbe $ dentro de um filtro.
// Funciona em implementações mais antigas; não permitido pela RFC 9535
$..book[?(@.price < $.expensive)]
// Abordagem padrão: passe o limiar pelo chamador
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
Expressões de script criam três problemas:
- A portabilidade cai a zero. Trocar de biblioteca significa reescrever cada expressão;
- A fronteira de segurança desaparece. Quando as expressões vêm dos usuários, a avaliação de script entrega execução de código;
- A otimização estática se torna impossível. A implementação só consegue interpretar nó por nó, o que é lento e difícil de fazer cache.
Se você realmente precisa de comparação entre nós, passe o limiar como parâmetro de consulta e filtre uma segunda vez no código da aplicação. Isso normalmente é mais estável do que depender de uma sintaxe de extensão.
6. Formas de resultado: você pensa que recebe valores, mas recebe nós
A mesma expressão pode retornar pelo menos três formas entre bibliotecas:
["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // array de valores
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // caminho + valor
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + valor
Isso afeta diretamente três coisas: como o frontend lê resultados, como os resultados são deduplicados e como os resultados de consulta viram operações de escrita. A nodelist definida pela RFC 9535 é um conjunto de referências de nó, mas implementações JavaScript normalmente retornam apenas valores porque referências não cruzam fronteiras de processo.
Conselho prático:
- Sempre leia um valor único por
[0]ouvalue(); nunca presuma que o resultado é um escalar; - Quando precisar gravar de volta, escolha uma implementação que retorna JSON Pointers ou construa o pointer você mesmo;
- Um resultado vazio e um resultado
[null]são diferentes; verifique o comprimento em vez da veracidade.
7. Diferenças entre bibliotecas: rode a suíte de conformidade antes de migrar
| Capacidade | Goessner original | jsonpath-plus | RFC 9535 estrita |
|---|---|---|---|
Parênteses de filtro ?() |
Obrigatórios | Compatível | Opcionais |
Referência cruzada à raiz $.x |
Suportada | Suportada | Proibida |
| Expressões de script | Suportadas | Suportadas | Proibidas |
| Índices e passos negativos | Suportados | Suportados | Suportados |
| Retornar caminhos | Configurável | Configurável | Não especificado |
Funções como length() |
Nenhuma | Extensões | Funções padrão |
Antes de migrar, verifique pelo menos quatro coisas: se parênteses são obrigatórios, se $ pode aparecer dentro de um filtro, como se comportam passos negativos e a estrutura do resultado. A suíte oficial de conformidade cobre a maioria das diferenças de sintaxe e evita falhas silenciosas como "três linhas desapareceram após trocar de biblioteca".
8. Desempenho e lista de uso indevido
O desempenho se resume em uma frase: JSONPath é uma travessia completa, e .. é o tipo mais caro de travessia completa.
- Para documentos com mais de 100.000 nós, estreite o escopo com um caminho explícito primeiro e depois filtre o subconjunto;
- Compile uma expressão uma vez e reutilize-a; não reagrupe a string dentro de um laço;
- Mude para
jqou código para agregação (somar, agrupar, ordenar); o JSONPath só seleciona; - Coloque as condições o mais cedo possível:
$.store.book[?(@.price < 10)]varre muito menos nós irrelevantes do que$..[?(@.price < 10)].
Lista de uso indevido:
- [ ] Tratar um teste de existência como um teste de veracidade booleana;
- [ ] Usar
$..em vez de uma profundidade conhecida; - [ ] Depender de expressões de script para comparação entre nós;
- [ ] Presumir que o resultado é sempre um array de valores;
- [ ] Permitir expressões controladas pelo usuário sem limites de comprimento e aninhamento.
Perguntas frequentes
Por que @.stock ainda corresponde quando o campo é false?
Porque um caminho cru dentro de um filtro significa "o campo existe", não "o valor é true". Para testar o booleano, compare explicitamente com @.stock == true. Essa é a maior diferença semântica entre JSONPath e a veracidade de JavaScript.
A ordem da descida recursiva é estável?
Não. A RFC 9535 especifica semântica de conjunto de nós, e a ordem de travessia depende de como a implementação visita os filhos. Quando precisar de uma ordem fixa, localize o array e leia por índice, ou ordene os resultados no código da aplicação.
Fatias e filtros podem ser combinados?
Sim, mas lembre-se do escopo. $.store.book[0:2][?(@.stock)] só filtra os dois livros produzidos pela fatia. Para filtrar primeiro e fatiar depois, escreva $.store.book[?(@.stock)][0:2]; uma ordem diferente dá um resultado diferente.
Por que jsonpath-plus suporta $.expensive quando o padrão proíbe?
Porque filtros são definidos apenas em termos do nó atual @. Uma referência cruzada à raiz faz uma expressão depender de todo o contexto do documento, o que é difícil de analisar estaticamente e de portar entre linguagens. Passe o limiar como parâmetro em vez de embuti-lo na expressão.
Como evito que a descida recursiva varra nós irrelevantes?
Escreva o caminho explícito sempre que souber a profundidade, estreite o escopo primeiro e filtre apenas o subconjunto em vez de usar $.. no lugar de um nível conhecido. Coloque as condições o mais cedo possível: $.store.book[?(@.price < 10)] varre muito menos nós irrelevantes do que $..[?(@.price < 10)]. Para documentos com mais de 100.000 nós, faça isso primeiro.