FAQ de consulta e validação: JSONPath e Schema
Resumo: Se consultas de caminho devem usar JSONPath ou Pointer, como escolher entre Patch e Merge Patch, o que o Schema consegue bloquear e como escolher uma versão de draft — tudo explicado em uma passada.

Resumo da ópera: use JSONPath para encontrar nós por condição, JSON Pointer ou JSON Patch para endereçar e editar localizações exatas, e Merge Patch para mesclar configuração; fixe Schemas novos em 2020-12 e ative allErrors para que toda falha volte em uma passada.
Consultar é "achar"; validar é "confirmar que está certo". Cada um tem seu próprio padrão, então não os misture.
1. Consultas de caminho
Como devo escolher entre JSONPath e JSON Pointer?
Depende se você está "achando por condição" ou "buscando por posição".
| Dimensão | JSONPath | JSON Pointer |
|---|---|---|
| Padrão | RFC 9535 (2024) | RFC 6901 (2013) |
| Expressão | $.users[?(@.age > 18)].id |
/users/0/id |
| Wildcards e recursão | Suporta *, .. |
Não suportado |
| Filtragem condicional | Suportada | Não suportada |
| Resultado | Um conjunto de nós (possivelmente vários) | Uma única localização |
| Expressividade | Forte, mas com um pesado legado de diferenças entre implementações | Fraca, mas totalmente determinística |
| Uso típico | Extração de logs, depuração, exploração de dados | JSON Patch, endereçamento de configuração, caminhos de erro |
A regra de seleção é direta:
- Precisa "pegar todos os nós que satisfazem uma condição" → JSONPath;
- Precisa "fixar uma localização exata e adicionar, remover ou modificar" → JSON Pointer;
- Use juntos: encontre o nó com JSONPath e descreva sua localização com Pointer.
Não use JSONPath para endereçamento em um contrato de API. JSONPath retorna um conjunto, e os três ramos para resultado vazio, de um elemento e de vários elementos complicam o protocolo; Pointer significa "ou ele aponta para uma localização existente ou não existe", o que convém melhor a um contrato.
Qual é a diferença entre $.. e $.*?
$.. é descida recursiva e percorre todos os níveis; $.* retorna apenas os filhos diretos do nível atual.
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ os dois objetos{id:1, child:{...}}e{id:3};$..id→ três valores:1,2,3;$.*.id→1e3, excluindo o2aninhado.
A descida recursiva é prática mas cara: $.. percorre a árvore inteira, o que é O(n) em um documento grande, e facilmente junta campos profundos que você não pretendia.
Se um nível concreto funciona, evite ...
Filtros podem comparar strings? Podem fazer correspondência com regex?
A comparação de strings funciona; regex depende da implementação.
Sintaxe padrão (RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // compara com outro nó
A comparação de strings usa aspas duplas (aspas simples também são permitidas na RFC 9535, mas algumas implementações mais antigas aceitam apenas aspas duplas).
A comparação numérica segue a semântica numérica; misturar uma string com um número, como "10" > 9, é comportamento indefinido no padrão, então não conte com isso.
Para regex, a RFC 9535 define as funções match() e search(), mas o suporte a elas varia consideravelmente entre implementações.
A abordagem segura: deixe o JSONPath fazer filtragem grosseira e faça a correspondência de regex no código depois de recuperar os resultados.
Devo usar JSON Patch ou Merge Patch?
Ambos descrevem "como mudar algo", mas sua expressividade e semântica são completamente diferentes.
| Dimensão | JSON Patch (RFC 6902) | Merge Patch (RFC 7386) |
|---|---|---|
| Forma | Um array de operações [{"op":"add",...}] |
Um objeto parcial |
| Operações suportadas | add / remove / replace / move / copy / test | Apenas merge (exclusão via null) |
| Endereça elementos de array | Sim, por índice | Não, o array inteiro é substituído |
| Atomicidade | test viabiliza bloqueio otimista |
Nenhuma |
| Tamanho do documento | Grande mas preciso | Pequeno mas limitado em expressividade |
| Bom para | Edições finas, atualizações parciais de API | Mesclagem de configuração, envio parcial de formulário |
Critérios de decisão:
- Mudar um campo do terceiro elemento de um array → JSON Patch;
- Mesclar várias entradas de configuração na configuração existente → Merge Patch;
- Ter controle de concorrência de "falhar se o valor original não for X" →
testdo JSON Patch.
Observe a armadilha na semântica de null do Merge Patch: null significa "exclua esta chave", não "defina o valor como null".
Definir um valor como null é impossível com Merge Patch.
2. Validação por Schema
Que problemas o JSON Schema resolve?
Ele transforma "como os dados deveriam parecer" de um acordo verbal em um contrato verificável por máquina. Os erros que ele consegue realmente bloquear:
- Campos obrigatórios ausentes (
required); - Tipo errado, por exemplo uma string onde se espera um número (
type); - Valores fora da faixa (
minimum/maximum/minLength); - Valores fora do conjunto permitido (
enum); - Tipos de elementos de array inconsistentes ou comprimento de array errado (
items/minItems); - Campos ausentes em estruturas aninhadas (validação recursiva por
properties); - Campos extras (
additionalProperties: false).
O que ele não resolve: regras de negócio ("o horário de fim deve ser posterior ao de início"), condições entre campos (algumas podem ser expressas com if/then,
mas a legibilidade sofre), unicidade em banco de dados e verificações de permissão.
Então o Schema é o porteiro da "forma dos dados", não um substituto da lógica de negócio.
Devo usar draft-07 ou 2020-12?
Use 2020-12 para projetos novos; para sistemas existentes, verifique o que o validador suporta.
| Versão | Situação | Mudanças-chave |
|---|---|---|
| draft-04 | Ultrapassada | Muitas bibliotecas antigas ainda usam por padrão |
| draft-07 | Padrão de fato | Suporte mais amplo, ecossistema mais rico |
| 2019-09 | Transitória | Introduziu $defs, unevaluatedProperties |
| 2020-12 | Recomendada atualmente | items dividido em prefixItems + items, suporta $dynamicRef |
Duas armadilhas de migração:
- A sintaxe de tupla mudou: no draft-07 um
itemscom valor de array expressava uma tupla; o 2020-12 mudou isso paraprefixItems; $refe palavras-chave irmãs: no draft-07 as palavras-chave irmãs são ignoradas quando$refestá presente; o 2020-12 permite que coexistam.
Se o validador só suporta draft-07, escrever sintaxe 2020-12 mesmo assim é ignorado silenciosamente — e o caso mais perigoso é "a validação passa" quando nada foi validado.
Como gero tipos TypeScript a partir de um Schema?
Duas rotas: derivar tipos do Schema, ou gerar o Schema a partir de tipos.
Do Schema para os tipos (o backend define o contrato, o frontend o consome):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
O segredo é o as const: sem ele, type: 'string' é inferido como string em vez do literal 'string', e a inferência desmorona.
Dos tipos para o Schema (o frontend já tem tipos e precisa de validação em tempo de execução):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
A vantagem desta rota é que "uma definição produz tanto os tipos quanto a validação em tempo de execução"; o custo é introduzir uma DSL,
e o Schema gerado pode carregar detalhes de implementação (por exemplo uma estrutura $ref pouco atraente).
Como localizo o campo específico quando a validação falha?
Olhe o instancePath (2020-12) ou dataPath (draft-07) retornado pelo validador; é um JSON Pointer para a localização do erro.
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price" diz diretamente: o campo price do terceiro elemento do array items tem o tipo errado.
Duas sugestões práticas:
- Retorne todos os erros de uma vez em vez de lançar no primeiro — o frontend corrigir tudo em uma passada é mais rápido do que tentativas de ida e volta;
- Transforme o Pointer em linguagem simples antes de mostrar ao usuário final;
/items/2/priceé útil para desenvolvedores e inútil para usuários.
allErrors: true é a chave na maioria dos validadores (o Ajv exige ativá-la explicitamente); o padrão costuma ser o modo fail-fast.
additionalProperties: false deve ser ativado?
Ative para contratos internos; seja cauteloso com APIs públicas.
Uma vez ativado, qualquer campo não declarado em properties falha na validação. O benefício é pegar erros de digitação
(userNmae) e repasses inesperados; a desvantagem é que uma adição de campo upstream quebra consumidores downstream, destruindo a compatibilidade retroativa.
Meio-termo:
| Cenário | Recomendação |
|---|---|
| Entre serviços internos | Ativar. Mudanças de campo passam por gerenciamento de versão |
| APIs externas públicas | Desativar. Permita que o upstream adicione campos |
| Consumir dados de terceiros | Desativar. Você não controla a outra ponta |
| Validação de configuração | Ativar. Um erro de digitação na configuração deve falhar imediatamente |
Como valido que os elementos de um array são únicos?
Duas semânticas diferentes — não as confunda:
{ "uniqueItems": true }
Isso deduplica o elemento inteiro, exigindo que o array não contenha dois elementos profundamente iguais.
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
O trecho acima só garante que os objetos como um todo não se dupliquem; {"id":"a","name":"x"} e {"id":"a","name":"y"} não contam como duplicatas.
Para deduplicar por um campo específico, o Schema não consegue expressar isso; você precisa validar na camada de negócio:
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id 重复');
}
O format realmente valida e-mails e datas?
Não necessariamente. Na especificação, format é uma "anotação" por padrão em vez de uma "asserção", então um validador pode coletá-lo sem validá-lo.
- O Ajv exige instalar e configurar o pacote separado
ajv-formats, caso contrárioformat: "email"é efetivamente inerte; - Alguns validadores tratam
formatcomo uma asserção ativada por padrão, então o comportamento é inconsistente; - Mesmo quando ativado, o regex de
emailé muito frouxo —a@bpassa.
Conclusão: não trate format como validação forte. Faça validação estrita de e-mails, URLs e datas no seu código de negócio;
manter format no Schema como documentação vale a pena, mas não espere que ele detenha dados sujos.
3. Referência rápida
| Pergunta | Conclusão |
|---|---|
| Encontrar nós por condição | JSONPath |
| Endereçamento preciso, aplicar patches | JSON Pointer |
| Mudar elementos de array | JSON Patch, Merge Patch não consegue |
| Mesclar configuração | Merge Patch |
| Versão de Schema para projetos novos | 2020-12, atenção ao prefixItems |
| Gerar tipos TS a partir do Schema | json-schema-to-ts + as const |
| Localizar erros de validação | Leia instancePath, ative allErrors |
| Deduplicar por campo | O Schema não consegue; use um Set na camada de negócio |
format: email |
A maioria dos validadores não o verifica por padrão, então não conte com ele |