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.
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+typesemnull - O campo pode estar ausente, mas quando presente não pode ser
null? → semrequired,typesemnull - O campo pode estar ausente ou ser
null? → semrequired,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.
- 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.