JSON Schema: por que você recebe "must be number" e como corrigir de vez
O erro 'must be number' do JSON Schema tem quatro causas reais em produção. Entenda por que acontece, como ler o instancePath e por que coerceTypes é a resposta errada.
O payload chegou. O JSON parseia sem erro. O schema está declarado. E mesmo assim o validador retorna must be number para um campo que claramente contém um número. A primeira reação é suspeitar do schema. A segunda é suspeitar do validador. Nenhuma das duas está certa.
O erro must be number em JSON Schema não é um bug do validador — é o validador fazendo exatamente o que deveria: rejeitando um dado que viola o contrato. O problema está em quem mandou o dado.
De onde vem o erro "must be number" na prática
Há quatro fontes que geram esse erro com frequência consistente o suficiente para justificar uma lista.
1. Formulários HTML
Um <form> com method="POST" sem Content-Type: application/json envia application/x-www-form-urlencoded por padrão. Todos os campos chegam como string no servidor, sem exceção. O campo amount com valor 150 no formulário chega como "150" no payload. Isso não é bug do browser — é o comportamento especificado.
O problema aparece quando o backend recebe esse form data, constrói um objeto JavaScript/Python sem converter os tipos, e tenta validar contra um schema com "type": "number". A validação falha porque a string "150" não é o número 150.
2. APIs que serializam número como string
Não é incomum. Sistemas legados frequentemente retornam campos financeiros como strings: "amount": "150.00" em vez de "amount": 150.00. Às vezes é uma decisão de design para preservar precisão decimal (ponto flutuante e dinheiro é uma discussão longa). Às vezes é descuido. O resultado para o consumidor é o mesmo: string em vez de número.
Se você está validando a resposta de uma API terceira contra um schema, esse é o caso mais provável.
3. Bancos de dados que serializam DECIMAL como string
ORMs e drivers de banco têm comportamento variado com tipos DECIMAL e NUMERIC. Dependendo do driver e da configuração, um campo DECIMAL(10,2) pode chegar como string "150.00" no objeto retornado pela query. MySQL e PostgreSQL têm behaviors diferentes aqui, e alguns drivers mudam o comportamento por versão.
O dado estava correto no banco. A serialização introduziu a conversão de tipo.
4. Variáveis de ambiente
process.env.RATE_LIMIT sempre é string, mesmo que você tenha escrito RATE_LIMIT=100 no .env. os.getenv('RATE_LIMIT') em Python, $_ENV['RATE_LIMIT'] em PHP — idem. Se você pega um valor de variável de ambiente e coloca num objeto que vai ser validado com JSON Schema, ele é string, não número.
Esse caso aparece em configuração de serviços: um schema que valida configuração lida de variáveis de ambiente vai falhar em todo campo numérico se você não converter explicitamente.
A diferença entre "type": "number" e "type": "integer"
JSON Schema tem dois tipos numéricos, e a confusão entre eles gera um tipo específico de erro.
"type": "number" aceita qualquer valor numérico JSON: 1, 1.5, 1.0, -42, 3.14. Não aceita a string "1".
"type": "integer" aceita apenas valores sem parte fracionária: 1, 42, -10. Não aceita 1.5. E um ponto contra-intuitivo: 1.0 é aceito como integer porque matematicamente é um inteiro — não há parte fracionária. JSON Schema avalia o valor matemático, não a representação textual.
A confusão mais comum: usar "type": "integer" para um campo de valor monetário que pode ter centavos. O schema rejeita 19.99 porque não é inteiro. A solução é "type": "number" — ou, dependendo da precisão necessária, representar valores monetários como centavos inteiros (já uma decisão de design de API).
Para testar o comportamento antes de integrar o schema no código, cole o payload e o schema no Validador JSON Schema — ele mostra o erro com instancePath exato e qual keyword falhou.
Como ler o instancePath para achar o campo culpado
O AJV retorna um array de erros, cada um com esta estrutura:
{
"instancePath": "/items/0/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"params": {
"type": "number"
},
"message": "must be number"
}
O instancePath é um JSON Pointer — cada segmento separado por / indica um nível na estrutura do seu dado. /items/0/price significa: dentro do array items, no primeiro elemento (índice 0), o campo price.
Se o instancePath estiver vazio (""), o erro está no objeto raiz, não num campo aninhado.
O schemaPath mostra onde no schema está a keyword que falhou — útil quando o schema usa $ref ou allOf e a origem real da regra não é óbvia de imediato.
Com allErrors: true no AJV, você recebe todos os erros de uma vez. Sem ele, o validador para no primeiro — o que é frustrante quando há múltiplos campos errados no payload.
coerceTypes: o atalho que vira dívida técnica
O AJV tem a opção coerceTypes: true. Com ela ativada, a string "150" passa na validação de "type": "number" — o validador converte silenciosamente.
A tentação é óbvia: o erro some, os testes passam, segue em frente.
O problema: o produtor do dado continua enviando string. Você não corrigiu o contrato — você fez o consumidor aceitar dado errado. Daqui a seis meses, alguém vai usar esse campo numa operação matemática sem saber que ele pode chegar como string em algum caminho de código que não passa pelo validador com coerção. O bug vai reaparecer, mais difícil de rastrear porque o schema "diz" que o campo é number mas na prática aceita string.
Coerção é útil em dois casos específicos: query strings de URL (tudo chega como string por definição e converter é razoável) e configuração de variáveis de ambiente (onde você controla o schema e a conversão é explícita e documentada). Fora disso, coerção mascara o bug — não o corrige.
A correção certa é converter o tipo no lugar certo: no parser do form data, no adapter da API terceira, no mapeador do resultado da query. Onde o dado entra no sistema, não onde ele é validado.
Quando string numérica é uma decisão de design
Às vezes você não controla o produtor. A API terceira retorna "amount": "150.00" e não vai mudar. Nesse caso, o schema deve refletir a realidade:
{
"amount": {
"type": "string",
"pattern": "^-?[0-9]+(\\.[0-9]+)?$"
}
}
Ou, se o campo pode ser string ou number dependendo da versão da API que você está consumindo:
{
"amount": {
"oneOf": [
{ "type": "number" },
{ "type": "string", "pattern": "^-?[0-9]+(\\.[0-9]+)?$" }
]
}
}
Mas documente explicitamente que isso é uma concessão ao produtor, não o contrato ideal. Quando você mudar de fornecedor ou a API for corrigida, o schema deve ser atualizado para aceitar apenas "type": "number".
Union type com string numérica num schema que você controla de ponta a ponta é um sinal de que algum lado do sistema não está cumprindo o contrato. Vale investigar antes de formalizar o desvio no schema.
Corrija o produtor, não o validador
O erro must be number tem uma causa raiz consistente: algo no pipeline de dados está produzindo um tipo errado. O form data que não é parseado, a API que serializa diferente do documentado, o ORM que converte silenciosamente, a variável de ambiente que não foi convertida.
O validador JSON Schema faz o trabalho dele quando rejeita esse dado. A resposta correta é investigar instancePath, identificar o campo, rastrear de onde ele veio no sistema, e converter o tipo na entrada — não flexibilizar o schema para aceitar o dado errado.
A função do schema é definir o contrato. Quando o contrato precisa ser dobrado para aceitar dado incorreto, o contrato perdeu o valor.
Nota: o conteúdo editorial acabou aqui. O que vem abaixo é uma indicação de ferramenta relacionada ao tema do post.
Ferramenta relacionada
Para reproduzir o erro antes de escrever código — cole o payload problemático e o schema no Validador JSON Schema. O resultado mostra o instancePath exato, a keyword que falhou e a mensagem completa do AJV. Útil especialmente quando o schema tem estruturas aninhadas e o campo culpado não é óbvio na stack trace.
- 01 diff antes de abrir PR: o hábito que reduz comentários de revisão Ler o próprio diff antes de abrir um PR muda o que você vê no código. Como e por que esse hábito reduz ciclos de revisão e melhora a qualidade dos PRs.
- 02 `P@ssw0rd1` passa na sua validação. Esse é o problema. O que o NIST mudou em 2024 sobre senhas: comprimento bate complexidade, leet speak está no rockyou.txt e rotação obrigatória enfraquece senhas.