JSON Toolkit

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.

Avançado~10 min de leituraPublicado em 2026-10-31Atualizado em 2026-10-31
Diagrama da localização de erros de validação do Ajv e governança de mensagens

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/price significa o campo price do primeiro elemento do array.
  • schemaPath começ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 $id para saber de qual arquivo veio.
  • Uma chave contendo / ou ~ é escapada para ~1 e ~0 respectivamente. 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:

  1. 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.
  2. Delegue plurais e classificadores à biblioteca de i18n. Nunca fixe "caracteres" em um template; 1 caractere e 3 caracteres diferem.
  3. 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.