Quando erros de JSON Schema não fazem sentido: localização e governança de mensagens do Ajv
Resumo: Um validador emite erros legíveis por máquina; usuários querem saber qual caixa de entrada está errada. Cobre o array de erros do Ajv, instancePath versus schemaPath, agrupamento e redação.

Resumo da ópera: leia instancePath primeiro para localizar os dados que falharam e depois schemaPath para localizar a regra que disparou; para required e additionalProperties, extraia o nome real da chave de params antes de escolher a mensagem.
O trabalho do validador é dizer qual regra falhou. Traduzir isso em "esta caixa de entrada está errada" é o seu trabalho.
1. O que o array de erros realmente lhe dá
O Ajv não lança erro em falha de validação; ele deixa o resultado em validate.errors. A configuração padrão guarda apenas o primeiro erro, e allErrors é o que produz uma lista completa. Um único objeto de erro tem cinco campos que importam, e entendê-los é a premissa para tudo a jusante:
| Campo | Significado | Exemplo |
|---|---|---|
instancePath |
JSON Pointer para o valor que falhou | /email |
schemaPath |
Localização da palavra-chave que falhou no schema | #/properties/email/format |
keyword |
Nome da restrição violada | format |
params |
Parâmetros legíveis por máquina dessa restrição | { "format": "email" } |
message |
Prosa em inglês que muda entre versões | must match format "email" |
Um erro real fica assim:
[
{
"instancePath": "/email",
"schemaPath": "#/properties/email/format",
"keyword": "format",
"params": { "format": "email" },
"message": "must match format \"email\""
}
]
Observe que message é prosa escrita para humanos, não uma interface estável. O Ajv pode reformulá-la entre versões, e ela não serve a um produto localizado. Nunca a analise com expressão regular, e nunca a retorne literalmente a um cliente.
2. instancePath e schemaPath respondem a perguntas diferentes
instancePath diz qual fatia dos dados falhou; schemaPath diz qual regra disparou. São dois sistemas de coordenadas, e não são simétricos:
instancePathé um JSON Pointer da RFC 6901. Uma string vazia significa a raiz;/items/0/pricesignifica o campo price do primeiro elemento do array.schemaPathcomeça com#e aponta para a palavra-chave dentro do documento de schema. Através de um$ref, ele aponta dentro do schema referenciado, então você precisa do$idpara saber de qual arquivo veio.- Uma chave contendo
/ou~é escapada para~1e~0respectivamente. Desfaça isso antes de exibir, ou a interface mostrará símbolos desconcertantes.
Por exemplo, instancePath: "/email" junto com schemaPath: "#/properties/email/format" significa que o campo email no objeto raiz não passou na verificação de formato.
A armadilha mais comum são required e additionalProperties: o instancePath deles aponta para o objeto pai, não para a chave problemática.
| Palavra-chave | instancePath aponta para |
Onde a chave real vive |
|---|---|---|
required |
O objeto pai (string vazia na raiz) | params.missingProperty |
additionalProperties |
O objeto pai | params.additionalProperty |
type |
O próprio valor que falhou | — |
enum |
O próprio valor que falhou | params.allowedValues |
Ao mapear erros para campos de formulário, trate primeiro estes dois casos. Caso contrário, "falta email" vira "erro no objeto raiz", e o usuário não faz ideia de onde digitar.
3. allErrors e verbose: completude versus custo
O allErrors: false padrão faz o Ajv parar na primeira falha. Isso é bom para uma ferramenta de linha de comando e desastroso para um formulário: o usuário corrige um campo, envia, vê o próximo erro, e repete três vezes até o formulário passar.
| Configuração | Comportamento | Benefício | Custo |
|---|---|---|---|
allErrors: false |
Apenas o primeiro erro | Mais rápido, menor memória | Envios repetidos |
allErrors: true |
Coleta todo erro | Todos os problemas em uma passada | A contagem de erros explode em schemas complexos |
verbose: true |
Anexa schema e data a cada erro |
Diagnóstico do lado do servidor mais fácil | Tamanho muito maior; nunca exponha |
messages: false |
Descarta o campo message |
Payloads menores | Perde a pista de depuração em inglês |
Divisão recomendada: habilite allErrors: true para validação no servidor e mantenha o array completo em logs internos, enquanto a resposta pública carrega apenas campos em whitelist. Ligue verbose apenas durante a investigação; ele embute o fragmento de schema correspondente e os dados do usuário em cada erro, o que custa memória e pode vazar valores sensíveis para logs.
O volume de erros é o outro problema comum. Um ramo anyOf que falha emite vários erros filhos mais um erro resumo, e oneOf com muitos ramos é pior. Mostre o array como está e os usuários verão cinco mensagens quase duplicadas para um campo. O agrupamento precisa acontecer antes da renderização.
4. Agrupe erros por campo
O agrupamento faz três coisas: transforma o caminho de volta em nome de campo, corrige o offset de required e additionalProperties, e mescla múltiplos erros no mesmo campo na única mensagem mais acionável.
/** Um erro de validação pronto para exibição */
interface FieldError {
field: string;
keyword: string;
params: Record<string, unknown>;
}
/**
* Agrupa erros do Ajv por campo.
* @param errors Array de erros produzido pelo Ajv
* @returns Mapa chaveado por caminho de campo, cada valor listando seus erros
*/
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
const grouped: Record<string, FieldError[]> = {};
for (const errorItem of errors) {
const keyword: string = errorItem.keyword;
let field: string = errorItem.instancePath;
if (keyword === 'required') {
const parentPath: string = errorItem.instancePath;
field = `${parentPath}/${errorItem.params.missingProperty}`;
} else if (keyword === 'additionalProperties') {
field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
}
const normalizedField = field === '' ? '(root)' : field;
(grouped[normalizedField] ??= []).push({
field: normalizedField,
keyword,
params: errorItem.params ?? {},
});
}
return grouped;
}
Mescle com uma ordem de prioridade: required (ausente) primeiro, depois type, depois enum/const, depois format/pattern, e por fim comprimento e faixa. Mostre apenas a mensagem de maior prioridade por campo e guarde o resto nos logs. O motivo é direto: quando o tipo está errado, uma mensagem de comprimento é ruído, e corrigir o tipo é o único próximo passo sensato.
Erros resumo de oneOf e anyOf normalmente não carregam informação acionável. Pule-os durante a renderização e mantenha apenas os erros de ramo que apontam para um campo concreto.
5. Mensagens customizadas: a rota do plugin e a rota do template
O Ajv não traz redação localizada, então há duas opções práticas.
Opção um: o plugin ajv-errors. Ele acrescenta uma palavra-chave errorMessage para que as mensagens vivam ao lado das restrições:
{
"properties": {
"email": {
"type": "string",
"format": "email",
"errorMessage": {
"format": "Digite um endereço de e-mail válido",
"type": "E-mail precisa ser uma string"
}
}
}
}
A vantagem é que a redação viaja com o campo e o schema serve de documentação. A desvantagem é que o texto fica congelado em uma língua, então localização significa manter vários schemas. O plugin também só substitui todas as mensagens quando allErrors: true está definido; esqueça essa flag e você terá uma mistura de mensagens customizadas e inglês padrão.
Opção dois: uma tabela de palavra-chave para template. O validador fornece apenas keyword e params; a aplicação produz a redação:
| Palavra-chave | Template | Parâmetros disponíveis |
|---|---|---|
required |
Campo obrigatório ausente: {field} | missingProperty |
type |
Tipo esperado: {type} | type |
minLength |
Digite pelo menos {limit} caracteres | limit |
maximum |
Não pode exceder {limit} | limit |
format |
Formato inválido: {format} | format |
enum |
Precisa ser uma das opções permitidas | allowedValues |
pattern |
O valor não corresponde ao formato esperado | pattern (não o ecoe) |
A opção dois convém a produtos que precisam de vários idiomas e de uma voz consistente. Ela separa validação de apresentação, mantém a tradução fora do schema e permite que o frontend reutilize os mesmos templates.
6. Localização: mensagens não pertencem ao validador
Mover a redação para pacotes de idioma é o ponto de partida da governança de mensagens. Um pacote de idioma armazena templates e chaves, nunca nomes de campo; rótulos de campo vêm da configuração do formulário ou do contrato de API, e os dois são combinados no momento da renderização.
/** Templates de mensagem de validação chaveados por palavra-chave do Ajv */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
required: (params) => `Campo obrigatório ausente: ${params.missingProperty}`,
type: (params) => `Tipo esperado: ${params.type}`,
minLength: (params) => `Digite pelo menos ${params.limit} caracteres`,
maximum: (params) => `Não pode exceder ${params.limit}`,
format: () => 'Formato inválido',
};
/**
* Constrói uma mensagem de validação voltada ao usuário.
* @param keyword Nome da restrição que falhou
* @param params Parâmetros da restrição
* @returns Texto da mensagem localizado
*/
function buildMessage(keyword: string, params: Record<string, any>): string {
const template = VALIDATION_TEMPLATES[keyword];
return template ? template(params) : 'O valor é inválido';
}
Três regras importam aqui:
- Não concatene frases. "Email format is invalid" e seu equivalente em chinês não compartilham ordem de palavras, e a montagem de strings acabará produzindo nonsense em um deles.
- Delegue plurais e classificadores à biblioteca de i18n. Nunca fixe "caracteres" em um template;
1 caracteree3 caracteresdiferem. - Traduza também os rótulos de campo.
params.missingPropertyéemail, enquanto a interface deve dizer "Endereço de e-mail". Mantenha esse mapeamento na configuração do formulário, não no pacote de idioma.
7. Não retorne estrutura interna a clientes
Retornar schemaPath, params e message juntos é conveniente durante o desenvolvimento e um vazamento de informação em produção. allowedValues revela enums internos, pattern revela regexes de validação, e schemaPath revela layout de arquivos e nomeação de campos — em alguns casos entregando aos atacantes um mapa dos seus pontos fracos.
A resposta pública mantém exatamente três coisas: um caminho de campo (ou nome de campo de formulário), um código de erro estável e texto voltado ao usuário. Use códigos curtos e estáveis em vez do keyword cru para manter espaço para trocar validadores depois.
{
"type": "about:blank",
"title": "Request validation failed",
"status": 422,
"errors": [
{ "field": "email", "code": "invalid_format", "message": "Digite um endereço de e-mail válido" },
{ "field": "items[0].price", "code": "invalid_pattern", "message": "O valor precisa ter exatamente duas casas decimais" }
]
}
Os logs do servidor devem guardar o oposto: o array de erros completo, o id da requisição e a versão do schema, para que uma investigação consiga responder "qual regra bloqueou isto". Evite despejar corpos de requisição inteiros nos logs; mantenha os caminhos e palavras-chave que falharam.
8. Apresentação no frontend: de destaque de campo a marcadores de editor
Os erros acabam chegando a uma interface, e a apresentação depende do contexto:
| Contexto | Apresentação | Ação-chave |
|---|---|---|
| Formulário | Texto vermelho sob o campo, foque no primeiro erro | Mapeie caminhos para nomes de formulário; mantenha índices de array |
| Importação em lote de tabela | Número da linha, nome da coluna, resumo, exportável | Transforme /items/3/price em linha 4, coluna de preço |
| Editor JSON | Marcadores inline e um painel de problemas | Use um analisador para mapear JSON Pointer para linha e coluna |
| Ferramenta de depuração de API | Lista de erros estruturada mantendo schemaPath |
Voltada a desenvolvedores, então mais detalhe é aceitável |
Três notas de implementação:
- Mostre apenas o primeiro erro por campo, colapsando o resto em "mais 2 problemas", para o formulário não se afogar em vermelho.
- Mantenha índices de array para mirarem mas descarte-os em rótulos. "Linha 4, preço" se lê melhor que
/items/3/price. - Cenários de editor precisam de mapeamento de posição. Uma busca simples de string é pouco confiável quando um nome de campo se repete; o analisador deve fornecer offsets para cada nó, que você então converte em linha e coluna.
Se o JSON vem da digitação de um usuário em vez de um formulário, confirme primeiro que o documento é sintaticamente válido. Erros de sintaxe acontecem durante a análise e nunca chegam à validação de schema. Análise e formatação JSON relata a linha e a coluna exatas, o que é muito mais rápido do que caçar erros de schema. Para como schemas estritos demais surgem quando são inferidos a partir de amostras, veja Por que o JSON Schema gerado automaticamente rejeita dados reais; para configuração de validador e o sistema de palavras-chave, veja JSON Schema: do básico à produção.
9. Perguntas frequentes
Por que recebo apenas um erro?
allErrors é false por padrão, então o Ajv retorna na primeira falha. Ligue-o explicitamente para validação de formulário; caso contrário os usuários precisam enviar repetidamente para descobrir todos os problemas.
Por que o caminho em um erro required está vazio?
O instancePath de required aponta para o objeto pai, que é a string vazia na raiz; o nome da chave ausente fica em params.missingProperty. Concatene os dois para obter o caminho completo. Mostrar instancePath sozinho produz o inútil "erro no objeto raiz".
Posso mostrar message diretamente aos usuários?
Não é aconselhável. É prosa em inglês cuja redação muda entre versões, e não tem capacidade de localização. Gere texto a partir de keyword e params e reserve message para logs.
Por que minha mensagem de erro customizada é ignorada?
Primeiro verifique se allErrors: true está habilitado; ajv-errors só consegue substituir todas as mensagens com essa flag. Depois confirme que errorMessage está no mesmo nível que a palavra-chave que falhou — um nível fora e ele não será reconhecido.
Quais campos são seguros para retornar em produção?
Caminho de campo, código de erro estável e texto voltado ao usuário. Mantenha schemaPath, params e message nos logs do servidor: isso apoia a depuração sem expor enums, regexes ou layout de arquivos internos.