JSON Toolkit

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.

Intermediário~12 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de A linguagem de consulta JSONPath na prática

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:

  1. Se parênteses de filtro são obrigatórios;
  2. Se índices negativos como -1 são suportados;
  3. 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.book primeiro e depois filtrar, não escreva $..book;
  • Compile expressões uma vez e reutilize-as (a maioria das bibliotecas oferece compile ou 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.color diretamente;
  • 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] ou value() 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.