Todos os artigos
74 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Tutoriais

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.

COVER · Tutoriais

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.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil