JSONPath, a versão que é de fato especificada
JSONPath foi um post de blog por dezessete anos. A RFC 9535 finalmente diz o que ele significa.
Cada afirmação desta página foi medida ou tem fonte. Quando não é nem uma coisa nem outra, a página diz isso.
JSONPath começou em 2007 como um post de blog de Stefan Goessner, esboçando uma linguagem de consulta ao estilo XPath para JSON em cerca de duas telas de prosa e uma implementação de referência em JavaScript. Foi bom o bastante para todo mundo implementar, e vago o bastante para todo mundo implementar de um jeito diferente. O operador de descendência .. considera o próprio nó raiz, ou só os filhos dele? [-1] é o último elemento ou um erro? Um colchete pode conter vários seletores? O que um filtro faz quando a chave que ele testa não existe? O que uma fatia com passo zero seleciona? Cada uma dessas perguntas tinha pelo menos duas respostas circulando por aí, e o documento original não resolvia nenhuma, porque partes dele eram delegadas a qualquer eval() que estivesse à mão.
A RFC 9535 consertou isso em fevereiro de 2024. É um Proposed Standard da IETF, o primeiro nível de maturidade da trilha de padrões: é uma especificação real, estável e redigida em termos normativos, e não é um Internet Standard. Trate como trataria qualquer Proposed Standard, isto é, como aquilo contra o que escrever código novo sabendo que muito código em produção é anterior.
O documento
Tudo abaixo roda contra isto:
{
"store": {
"name": "Corner Books",
"book": [
{ "category": "reference", "author": "Nigel Rees",
"title": "Sayings of the Century", "price": 8.95 },
{ "category": "fiction", "author": "Evelyn Waugh",
"title": "Sword of Honour", "price": 12.99 },
{ "category": "fiction", "author": "Herman Melville",
"title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 },
{ "category": "fiction", "author": "J.R.R. Tolkien",
"title": "The Lord of the Rings", "isbn": "0-395-19395-8" }
]
}
}
Repare no último livro: sem price. É nessa ausência que mora quase todo o comportamento interessante.
Segmentos e seletores
Uma consulta é $ seguido de uma sequência de segmentos. $ é a raiz. Cada segmento aplica um ou mais seletores a cada nó que se tem em mãos e produz uma nova lista de nós. O resultado de uma consulta é sempre uma lista de nós, mesmo quando contém um só ou nenhum, e é por isso que uma biblioteca de JSONPath devolve um array onde uma de JSON Pointer devolve um valor.
$.store.book[0].title notação de ponto, atalho para seletores de nome
$['store']['book'][0] notação de colchetes, significado idêntico
$["store"]["book"][0] aspas duplas também funcionam
A notação de colchetes não é enfeite opcional. $.first-name não é um seletor de nome válido, então uma chave com hífen, espaço, ponto ou dígito inicial precisa ser escrita $['first-name']. A forma com colchetes e aspas simples é também o formato de um caminho normalizado, o identificador único que a RFC 9535 define para um único nó: $['store']['book'][0]['title'].
Os seletores:
- Nome:
'title'ou"title", selecionando um membro de objeto. Nada num array. - Curinga
*: todo valor membro de um objeto, todo elemento de um array.$.store.book[*]e$.store.book.*são a mesma consulta. - Índice: um inteiro, base zero. Negativos contam do fim, então
[-1]é o último elemento. Isso agora está especificado, não é gentileza de cada biblioteca. - Fatia
início:fim:passo: semiaberta, fim exclusivo, e um passo negativo anda para trás. Um passo de0não seleciona nada em vez de levantar erro, o único ponto em que a RFC se separa de propósito do Python. - Filtro
?expr: coberto abaixo.
Um segmento filho pode conter vários seletores separados por vírgula, e eles não precisam ser do mesmo tipo. $.store.book[0, -1] é o primeiro e o último livro; $.store.book[0, 2:4] mistura um índice com uma fatia. Os resultados voltam na ordem dos seletores, então uma união pode legitimamente devolver o mesmo nó duas vezes.
Um segmento de descendência se escreve com dois pontos: $..author, $..['author'], $..*, $..[0]. Ele visita o nó de entrada e todo descendente dele, e então aplica os seletores a cada um. A velha ambiguidade acabou: $..store no documento acima casa com $.store, porque o segmento de descendência começa na própria raiz.
Filtros, que é onde estão as perguntas
Dentro de um filtro, @ é o nó corrente em teste e $ continua sendo a raiz do documento inteiro, então um filtro pode comparar um valor com algo em outro lugar do documento.
Uma consulta nua usada como expressão de filtro é um teste de existência: é verdadeira quando a consulta seleciona pelo menos um nó. $.store.book[?@.isbn] seleciona os dois livros que têm ISBN. Os operadores de comparação são ==, !=, <, <=, >, >=; os lógicos são &&, || e o prefixo !, com parênteses para agrupar. Parênteses em torno da expressão inteira são permitidos mas não mais obrigatórios, então tanto [?(@.price < 10)] quanto [?@.price < 10] são válidos e significam a mesma coisa.
Os operandos de comparação são restritos. Cada lado precisa ser um literal, uma consulta singular (feita só de seletores de nome e índice, de modo que possa selecionar no máximo um nó) ou uma chamada de função. @.price se qualifica. @..price e @.book[*].price não, e uma implementação deveria rejeitar a consulta em vez de chutar.
Agora a regra que derruba as pessoas. Uma consulta que não seleciona nada produz o valor especial Nothing, e Nothing não é null, nem zero, nem false. Ele compara igual a Nothing e a mais nada, e toda comparação de ordem que o envolva é falsa. Consequências:
$.store.book[?@.price < 10] exclui o livro do Tolkien (sem price)
$.store.book[?@.price >= 10] também o exclui
$.store.book[?@.price == null] também o exclui: Nothing não é null
$.store.book[?!@.price] seleciona ele, e só ele
Então a ausência se testa negando o teste de existência, e == null testa um membro que está presente e contém null. A imagem espelhada é uma surpresa genuína: $.store.book[?@.price == @.discount] seleciona o livro do Tolkien, porque os dois lados são Nothing e Nothing é igual a Nothing.
Uma comparação entre dois tipos diferentes nunca é erro. Igualdade entre tipos é simplesmente falsa, e ordem só é definida entre dois números ou duas strings, então @.price > "10" é falso para todo livro.
Extensões de função
Cinco estão definidas, e são tipadas, então length(@.book[*]) é erro de tipo em vez de surpresa em tempo de execução.
| Função | Recebe | Dá |
|---|---|---|
length() |
um valor | valores escalares Unicode numa string, elementos num array, membros num objeto, caso contrário Nothing |
count() |
uma lista de nós | quantos nós ela selecionou |
match() |
uma string e uma regex | verdadeiro se a regex casa com a string inteira |
search() |
uma string e uma regex | verdadeiro se a regex casa em qualquer parte da string |
value() |
uma lista de nós | o valor, se a lista tem exatamente um nó, caso contrário Nothing |
count() existe porque uma consulta não singular não pode ser operando de comparação, então count(@.book[?@.isbn]) == 2 é como se diz “tem exatamente dois livros com ISBN”. value() resolve o mesmo problema pelo outro lado: $[?value(@..name) == 'Corner Books'] funciona porque value() colapsa uma consulta de vários nós num único valor comparável, ou em Nothing se ela casou com mais ou menos de um nó.
O dialeto de regex é o I-Regexp (RFC 9485), um subconjunto deliberadamente pequeno que mapeia para expressões regulares do XSD. Não é PCRE. Não espere lookaheads nem retrorreferências, e lembre que match(@.category, 'fic') é falso para "fiction" enquanto search(@.category, 'fic') é verdadeiro.
O que não está na linguagem
Três coisas que as pessoas ainda procuram não existem. As expressões de script, a forma [(...)] do post original, se foram, e com elas a dependência de eval(). Não há operador de pai: uma consulta só anda para baixo, então se você precisa do objeto que contém, selecione o objeto e filtre pelo filho. E a pseudopropriedade length acabou, então $.store.book[(@.length-1)] não é uma consulta. Escreva $.store.book[-1].
Exemplos resolvidos
| Expressão | Resultado |
|---|---|
$.store.name |
"Corner Books" |
$.store.book[*].author |
os quatro autores |
$..isbn |
as duas strings de ISBN |
$.store.book[-1].title |
"The Lord of the Rings" |
$.store.book[1:3].title |
"Sword of Honour", "Moby Dick" |
$.store.book[::2].title |
"Sayings of the Century", "Moby Dick" |
$.store.book[0,-1].title |
"Sayings of the Century", "The Lord of the Rings" |
$.store.book[?@.price < 10].title |
"Sayings of the Century", "Moby Dick" |
$.store.book[?!@.price].title |
"The Lord of the Rings" |
$.store.book[?@.price > $.store.book[0].price].title |
"Sword of Honour", "Moby Dick" |
$.store.book[?match(@.category, 'fic.*')].author |
Waugh, Melville, Tolkien |
$.store.book[?search(@.author, 'Mel')].title |
"Moby Dick" |
$.store.book[?length(@.title) > 16].title |
"Sayings of the Century", "The Lord of the Rings" |
Cole o documento e qualquer uma destas no testador de JSONPath para ver a lista de nós ao lado do caminho normalizado de cada correspondência, que é a forma mais rápida de conferir se a sua biblioteca concorda com a RFC em índices negativos e chaves ausentes.
Quando usar outra coisa
JSONPath seleciona nós. É esse o trabalho inteiro, e três outras ferramentas de consulta se sobrepõem a ele:
JSON Pointer (RFC 6901) endereça exatamente um nó, sem curingas, sem filtros e sem ambiguidade: /store/book/0/title, com ~1 para uma barra literal e ~0 para um til literal. É com isso que os erros de JSON Schema e as operações de JSON Patch apontam. Se você sabe o endereço, use um Pointer.
jq é uma linguagem completa de processamento de fluxo com o próprio modelo de valores, aritmética, variáveis e formatação de saída. Ela transforma; o JSONPath só seleciona. Se a sua expressão está começando a construir objetos novos, o que você quer é jq.
JMESPath fica entre os dois: uma linguagem de consulta especificada, mais antiga que a RFC 9535, com projeções e a própria biblioteca de funções, e sintaxe próxima o bastante do JSONPath para confundir e diferente o bastante para quebrar. Escolha uma por base de código.
Para o caso do dia a dia, cortar um payload até os campos que interessam, a ferramenta de filtro faz isso sem linguagem de expressão nenhuma, e o visualizador te mostra a forma que você está consultando. Se o que você quer é saber o que mudou entre dois payloads, isso é diff, não consulta.