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

JSON Schema required: o que realmente valida (e o que não valida)

A keyword required em JSON Schema valida presença de chave, não de valor. Entenda por que null passa em campos required e como fechar esse contrato corretamente.

COVER · Tutoriais

Tinha um campo email marcado como required no schema. O payload chegou com "email": null. A validação passou. O email nulo foi persistido no banco. O usuário ficou sem receber o e-mail de confirmação, e só descobrimos uma semana depois olhando para uma fila de envios acumulando falha.

O schema estava declarado corretamente — o campo estava em required. O problema é que required não significa o que a maioria dos devs assume que significa.


O que required realmente valida

A keyword required em JSON Schema valida apenas uma coisa: a presença da chave no objeto. Não valida o valor. Não garante que o valor é não-nulo. Não impede que o campo exista com valor null, "", 0, ou qualquer outro valor que você consideraria "vazio".

Se o schema define:

{
  "type": "object",
  "properties": {
    "email": { "type": "string" }
  },
  "required": ["email"]
}

Esses três payloads têm comportamentos diferentes:

{ "name": "Rafael" }

❌ Falha — a chave email está ausente.

{ "email": null }

✅ Passa — a chave email existe. O valor null não é validado pela keyword required.

{ "email": "rafael@exemplo.com" }

✅ Passa — chave presente, valor correto.

O segundo caso surpreende. A especificação é clara: required é sobre a presença da propriedade, não sobre o valor que ela carrega. null em JSON não é ausência — é um valor explícito.


Campo obrigatório que não aceita null

Para garantir que um campo obrigatório não seja nulo, você precisa de duas coisas separadas: required para exigir a presença da chave, e type para restringir o valor.

{
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "format": "email"
    }
  },
  "required": ["email"]
}

Com "type": "string", o valor null falha porque null não é string. O schema agora rejeita tanto a ausência da chave quanto o valor nulo.

A confusão vem de ler required como "obrigatório e não-nulo". Na especificação, required significa apenas "obrigatório" no sentido de presença. A restrição de valor é responsabilidade do type.


Campo opcional que aceita null

O inverso também tem uma pegadinha. Se você quer um campo que não é obrigatório, mas quando presente pode ser null ou uma string:

{
  "properties": {
    "middle_name": {
      "type": ["string", "null"]
    }
  }
}

Campo não está em required, então pode estar ausente. Quando presente, aceita null ou string. Este é o padrão correto para campos verdadeiramente opcionais onde o consumidor pode enviar explicitamente null para indicar "sem valor".

Se você declarar apenas "type": "string" sem o required, o campo pode estar ausente — mas quando presente, não pode ser null. Essa distinção é relevante para APIs que diferenciam "campo ausente" de "campo explicitamente nulo" (JSON Merge Patch, por exemplo, usa null para indicar deleção).


O campo está em required mas sem type — o que acontece?

{
  "type": "object",
  "properties": {
    "amount": {}
  },
  "required": ["amount"]
}

Um schema de propriedade vazio {} é um schema válido que aceita qualquer valor — incluindo null, false, 0, "", ou um objeto aninhado. required só exige que a chave amount exista. O valor pode ser literalmente qualquer coisa.

Esse padrão aparece quando alguém adiciona um campo ao required sem definir as restrições dele. É um contrato que diz "esse campo deve existir" mas nada sobre o que ele deve conter. Não é o que você quer em produção.


required em objetos aninhados — onde as pessoas erram

required só se aplica ao nível do schema onde é declarado. Se você tem um objeto aninhado, precisa declarar required nele também:

{
  "type": "object",
  "properties": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" }
      },
      "required": ["street", "city"]
    }
  },
  "required": ["address"]
}

required: ["address"] no nível raiz garante que o objeto address existe. O required: ["street", "city"] dentro do schema de address garante que esses campos existem dentro do objeto aninhado. São dois required independentes em dois níveis diferentes.

Sem o required interno, você pode receber "address": {} — um objeto vazio que passa na validação do required do nível raiz porque a chave address existe.


required com additionalProperties: false — a combinação defensiva

required define o mínimo que deve estar presente. additionalProperties: false define o máximo — nenhuma chave fora das declaradas em properties é aceita. Usados juntos, eles fecham o contrato de ambos os lados:

{
  "type": "object",
  "properties": {
    "customer_id": { "type": "string", "format": "uuid" },
    "amount": { "type": "number", "minimum": 0.01 },
    "currency": { "type": "string", "enum": ["BRL", "USD", "EUR"] }
  },
  "required": ["customer_id", "amount", "currency"],
  "additionalProperties": false
}

Esse schema rejeita payloads com campos faltando (via required) e payloads com campos extras (via additionalProperties: false). O contrato é exato: nem mais, nem menos.

Para validar esse schema antes de integrar no código, use o Validador JSON Schema — cole o schema e diferentes variações de payload para confirmar que os casos de borda se comportam como você espera.


required e o campo default

Uma confusão frequente: se um campo tem default definido no schema, ele ainda precisa estar em required?

{
  "properties": {
    "status": {
      "type": "string",
      "enum": ["active", "inactive"],
      "default": "active"
    }
  }
}

Sim — default não remove a necessidade de required se você quer que o campo seja obrigatório. default em JSON Schema é um metadado informativo, não um valor aplicado automaticamente pelo validador. O AJV tem uma opção useDefaults: true que aplica defaults durante a validação, mas isso é comportamento do validador, não da especificação. Se você depende de default sem useDefaults, o campo ausente passa na validação mas o default não é preenchido.


O required certo é o que reflete o contrato real

O erro mais comum não é usar required incorretamente — é não pensar sobre o que o campo realmente exige. Antes de marcar um campo como required, responda:

  • A requisição deve falhar se esse campo está ausente? → required
  • A requisição deve falhar se esse campo é null? → required + type sem null
  • O campo pode estar ausente, mas quando presente não pode ser null? → sem required, type sem null
  • O campo pode estar ausente ou ser null? → sem required, type: ["string", "null"]

Cada combinação é um contrato diferente. JSON Schema tem keywords para todos eles — mas é você quem precisa escolher qual contrato quer impor.


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 os casos de borda do required antes de integrar num validador — payload sem o campo, com null, com string vazia — use o Validador JSON Schema. Cole o schema e os diferentes payloads para confirmar o comportamento exato de cada combinação.

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