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

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.

COVER · Tutoriais

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_id pode ser UUID válido mas não existir no banco
  • Consistência entre campos: start_date pode ser anterior a end_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.

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