JSON Toolkit

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.

Intermediário~10 min de leituraPublicado em 2026-10-31Atualizado em 2026-10-31
Capa de Filtros JSONPath em profundidade

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:

  1. $..book encontra cada nó chamado book, independentemente da profundidade;
  2. A travessia é em profundidade, e a ordem de saída depende de como a implementação visita os filhos;
  3. .. 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 < 10 nã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:

  1. A portabilidade cai a zero. Trocar de biblioteca significa reescrever cada expressão;
  2. 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;
  3. 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] ou value(); 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 jq ou 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.