JSON Schema na prática: validar antes de processar
JSON Schema não valida se o JSON está bem formado — valida se ele respeita o contrato que você definiu. Como implementar validação de entrada em Node.js e Python, e por que additionalProperties: false importa.
Recebi um bug em produção que levou duas horas para rastrear. Um campo amount chegando como string "150.00" em vez de número — o banco de dados aceitou, o ORM não reclamou, mas o cálculo de desconto silenciosamente retornou zero. O payload era sintaticamente JSON válido. O problema era estrutural.
JSON Schema resolve exatamente isso: não valida se o JSON está bem formado, mas se ele respeita o contrato que você definiu. Esse post cobre o que você precisa saber para implementar validação de entrada na prática — sem tutorial de "o que é JSON", direto para o que funciona em produção.
O problema que JSON Schema resolve
Toda API que recebe dados de fora tem um contrato implícito: "espero um objeto com esses campos, esses tipos, essas restrições". Na maioria das implementações esse contrato vive só na cabeça do dev que escreveu o endpoint — ou, na melhor das hipóteses, num comentário acima da função.
Quando o contrato não é validado explicitamente na entrada, o dado inválido viaja pelo sistema até algum lugar onde ele quebra de forma inesperada. Se você tem sorte, quebra com exceção. Se não tem, persiste silenciosamente e aparece como dado corrompido semanas depois.
JSON Schema externaliza esse contrato numa estrutura legível por máquina e por humano. A validação acontece antes da lógica de negócio — na borda da requisição, não no meio do processamento.
As keywords que cobrem 90% dos casos
Você não precisa dominar a especificação inteira. Estas são as que vão aparecer em todo schema que você escrever:
type — define o tipo esperado: "string", "number", "integer", "boolean", "array", "object", "null". Sem isso, qualquer valor passa.
properties — descreve os campos de um objeto e os schemas de cada um deles.
required — array com os nomes dos campos obrigatórios. Campos não listados aqui são opcionais por padrão.
additionalProperties — controla se campos não declarados em properties são permitidos. O valor padrão é true — ou seja, por padrão você aceita qualquer campo extra que chegue no payload. Definir como false é uma medida defensiva importante.
enum — lista de valores permitidos. Útil para campos de status, tipo, categoria.
format — validação semântica: "email", "date", "date-time", "uri", "uuid". Atenção: format não é validado por padrão em todas as implementações — no ajv você precisa de ajv-formats.
minimum / maximum, minLength / maxLength, pattern — restrições numéricas e de string.
Um schema real: validando um pedido
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"format": "uuid"
},
"amount": {
"type": "number",
"minimum": 0.01
},
"currency": {
"type": "string",
"enum": ["BRL", "USD", "EUR"]
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"product_id": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["product_id", "quantity"],
"additionalProperties": false
}
}
},
"required": ["customer_id", "amount", "currency", "items"],
"additionalProperties": false
}
Esse schema rejeita amount: "150.00" (string em vez de número), rejeita currency: "GBP" (fora do enum), rejeita itens sem product_id, e rejeita qualquer campo desconhecido no nível raiz do objeto.
Implementando em Node.js com ajv
Ajv é a implementação de referência para JavaScript — mais rápida e mais aderente à especificação do que as alternativas.
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)
// Compile UMA vez, fora do handler
const validateOrder = ajv.compile(orderSchema)
// Dentro do handler
function createOrder(req, res) {
const valid = validateOrder(req.body)
if (!valid) {
return res.status(400).json({
error: 'invalid_payload',
details: validateOrder.errors
})
}
// processa aqui — req.body já está validado
}
O ponto crítico: ajv.compile(schema) é uma operação cara. Se você chamar dentro do handler, você recompila a cada requisição. O custo é real — benchmarks mostram throughput 50x menor. Compile uma vez no escopo do módulo e reutilize a função retornada.
allErrors: true faz o ajv continuar validando após o primeiro erro — o cliente recebe todos os problemas de uma vez em vez de corrigir um por um.
Implementando em Python com jsonschema
from jsonschema import validate, ValidationError, Draft7Validator
validator = Draft7Validator(order_schema)
def create_order(payload: dict):
errors = list(validator.iter_errors(payload))
if errors:
raise ValidationError(
message="invalid_payload",
context=errors
)
# processa aqui
iter_errors retorna todos os erros de uma vez — mesmo comportamento do allErrors: true no ajv. Draft7Validator compilado uma vez fora da função, mesmo raciocínio de performance.
additionalProperties: false — a keyword que a maioria esquece
Por padrão, JSON Schema aceita campos extras silenciosamente. Um payload com { "amount": 100, "admin": true } passa numa schema sem additionalProperties: false. Dependendo do seu ORM ou mapeador, esse campo extra pode ser ignorado ou, em implementações menos seguras, processado.
Definir additionalProperties: false em todos os objetos do schema é uma postura defensiva: você declara explicitamente o contrato e qualquer campo fora dele é rejeitado. Isso também facilita evolução de API — quando você adiciona um campo novo, você o adiciona ao schema primeiro, não descobre depois que estava chegando silenciosamente.
O que JSON Schema não valida
JSON Schema valida estrutura e tipo. Não valida:
- Unicidade de negócio:
customer_idpode ser UUID válido mas não existir no banco - Consistência entre campos:
start_datepode ser anterior aend_date— você precisa de lógica custom para isso - Autenticação e autorização: quem mandou o payload, não o que tem dentro dele
- Lógica condicional complexa: dá pra modelar com
if/then/else, mas fica frágil para regras de negócio reais
A regra é: JSON Schema cuida do envelope. A lógica de negócio cuida do conteúdo. Os dois juntos, nessa ordem.
Validar na borda, confiar no interior
A separação que funciona em produção é simples: valide na entrada, não distribua validação pelo sistema. Se o dado passou pela validação de schema na borda da requisição, o restante do código pode assumir que a estrutura está correta — sem if payload.get('amount') is not None espalhado por todo o handler.
Isso não é só organização. É contrato explícito entre o que entra no sistema e o que o sistema processa. O bug do amount como string que mencionei no início não teria chegado ao banco se o schema estivesse em vigor. Dois horas de debug que não precisavam existir.
Nota: o conteúdo editorial acabou aqui. O que vem abaixo é uma indicação de ferramenta relacionada ao tema do post.
Ferramenta relacionada
Para testar e iterar um schema antes de integrar no código, uso o Validador JSON Schema — você cola o schema e o payload, e vê os erros com path exato e mensagem legível. Útil especialmente quando o schema tem $ref ou estruturas aninhadas e você precisa confirmar que a validação está pegando o que você espera.
- 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.