JSON Schema: do básico à produção
Resumo: Há muitas palavras-chave, mas só uma dúzia é usada todos os dias. Acerte `type`/`properties`/`required`/`$ref`, trate bem as mensagens de erro, e você cobre oitenta por cento dos casos reais.

Resumo da ópera: fixe $schema em 2020-12 primeiro e depois extraia estruturas compartilhadas com $defs + $ref; use oneOf apenas quando exatamente um ramo precisar corresponder, e prefira discriminadores ou anyOf em todos os outros lugares.
Um schema não é um apêndice da documentação; é um ativo que pode ser executado, testado e gerado.
1. Acerte primeiro a versão
| Versão | Valor de $schema |
Situação | Observações |
|---|---|---|---|
| draft-04 | http://json-schema.org/draft-04/schema# |
Ultrapassada | Projetos legados e algumas ferramentas OpenAPI 2.0 |
| draft-07 | http://json-schema.org/draft-07/schema# |
Amplamente usada | A base de fato do OpenAPI 3.0 |
| 2019-09 | https://json-schema.org/draft/2019-09/schema |
Disponível | Introduziu $defs e o vocabulário de anotação |
| 2020-12 | https://json-schema.org/draft/2020-12/schema |
Recomendada atualmente | Introduziu prefixItems, mudou a semântica de items |
Use 2020-12 para projetos novos. Observe que no 2020-12 items só restringe os "elementos restantes" — declarações no estilo tupla foram movidas para prefixItems.
Essa é a armadilha mais comum ao migrar do draft-07.
2. Um schema mínimo utilizável
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbdrug.com/schemas/user.json",
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 64 },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
},
"additionalProperties": false
}
Este schema cobre as quatro preocupações mais comuns: restrições de tipo, campos obrigatórios, limites de faixa e restrições de elementos de array.
3. Mapa de palavras-chave
3.1 Geral
| Palavra-chave | Finalidade | Cuidado |
|---|---|---|
type |
O tipo; pode ser um array | "type": ["string", "null"] significa anulável |
enum |
Valores enumerados | Os valores precisam ser únicos |
const |
Restrição de valor único | Bom para valores fixos como um número de versão |
$ref |
Referencia outro schema | Referencia o schema inteiro e não se mescla com palavras-chave irmãs no objeto que referencia |
3.2 Números
multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum.
Observe que no draft-04 exclusiveMaximum era um booleano (usado junto com maximum); só a partir do draft-07 ele se tornou uma palavra-chave numérica autônoma —
o que faz muitos "schemas copiados de documentos antigos" falharem no modo estrito.
3.3 Strings
minLength, maxLength, pattern (uma expressão regular ECMA-262) e format (uma anotação).
minLength conta pontos de código Unicode, não unidades UTF-16 nem bytes. Validar com o string.length do JavaScript discordará dele.
3.4 Arrays
| Palavra-chave | Finalidade |
|---|---|
prefixItems |
A restrição no estilo tupla do 2020-12, casando itens um a um por índice |
items |
Uma restrição uniforme sobre os elementos restantes (2020-12) |
contains + minContains/maxContains |
Pelo menos/no máximo quantos itens correspondentes devem estar presentes |
uniqueItems |
Os elementos precisam todos diferir (comparação profunda; evite em arrays grandes) |
minItems / maxItems |
Restrições de comprimento |
3.5 Objetos
properties, required, patternProperties, additionalProperties, propertyNames,
minProperties, maxProperties, dependentRequired, dependentSchemas.
Três confusões de alta frequência:
requiredsó exige que a chave exista:{"name": ""}é válido. Para rejeitar uma string vazia você precisa acrescentarminLength: 1;additionalProperties: falseamplia problemas de compatibilidade: um campo extra de um cliente falha o payload inteiro, então evoluir a API obriga todo chamador a atualizar em sintonia;propertiesnão restringe chaves não declaradas — por padrão elas são permitidas.
3.6 Composição lógica
{
"oneOf": [
{ "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
{ "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
]
}
| Palavra-chave | Semântica | Falha típica |
|---|---|---|
allOf |
Todos precisam ser satisfeitos | Múltiplos ramos impõem restrições conflitantes ao mesmo campo |
anyOf |
Pelo menos um precisa ser satisfeito | A mensagem de erro é difícil de rastrear até um ramo específico |
oneOf |
Exatamente um precisa ser satisfeito | Os dados correspondem a dois ramos e falham, embora "pareçam obviamente corretos" |
not |
Não pode ser satisfeito | Raramente usado, e as mensagens de erro menos amigáveis |
oneOf é a palavra-chave mais propensa a produzir "dados válidos que falham na validação". Se as restrições dos ramos se sobrepõem (por exemplo, ambos permitem campos extras),
um único registro corresponde a vários ramos e oneOf falha de vez. Nesse caso, mude para anyOf, ou use um campo discriminante com const dentro dos ramos.
{
"type": "object",
"required": ["kind"],
"properties": { "kind": { "enum": ["text", "image"] } },
"if": { "properties": { "kind": { "const": "image" } } },
"then": { "required": ["url", "width", "height"] },
"else": { "required": ["content"] }
}
O estilo com discriminador é mais legível que oneOf e produz erros melhores.
4. Dividindo e reutilizando
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbdrug.com/schemas/order.json",
"$defs": {
"money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
"address": {
"type": "object",
"required": ["city"],
"properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
}
},
"type": "object",
"required": ["total", "shipTo"],
"properties": {
"total": { "$ref": "#/$defs/money" },
"shipTo": { "$ref": "#/$defs/address" }
}
}
$defsé a forma introduzida no 2019-09; o draft-07 usadefinitions;- Referências entre arquivos usam
$ids relativos, mas não conte com buscar schemas remotos em tempo de execução em produção — empacote-os como arquivos estáticos; - Você não consegue acrescentar restrições ao lado de uma referência (o comportamento de
$refcom palavras-chave irmãs varia por versão). Quando precisar de composição, useallOf.
5. Validação em tempo de execução com Ajv
import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };
const ajv = new Ajv2020({
allErrors: true,
strict: true,
allowUnionTypes: true,
});
addFormats(ajv);
const validateOrder = ajv.compile(orderSchema);
export function assertOrder(input: unknown): void {
if (validateOrder(input)) {
return;
}
const detail = (validateOrder.errors ?? []).map((errorItem) => ({
path: errorItem.instancePath || '/',
keyword: errorItem.keyword,
message: errorItem.message,
params: errorItem.params,
}));
throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}
Pontos-chave:
- Chame
compileuma única vez, no nível superior do módulo; recompilar a cada requisição transforma uma validação de 10 ms em centenas de milissegundos; strict: truerevela cedo palavras-chave mal escritas (por exemplominlengthem vez deminLength), então vale habilitar;- Só
ajv-formatsfazformat: "email"eformat: "date-time"realmente entrarem em vigor; - O 2020-12 exige o ponto de entrada
ajv/dist/2020— não use o export padrão.
6. Como projetar mensagens de erro
O que o validador lhe dá é "qual palavra-chave falhou"; o que o usuário quer é "qual caixa de entrada está errada". Vale acrescentar uma camada de mapeamento:
const FIELD_LABEL: Record<string, string> = {
'/name': 'Nome',
'/age': 'Idade',
'/tags': 'Tags',
};
/**
* Converte erros do Ajv em mensagens prontas para exibição.
* @param errors A lista de erros do Ajv
* @returns Um array de mensagens de exibição
*/
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
return errors.map((errorItem) => {
const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
if (errorItem.keyword === 'required') {
const missing = (errorItem.params as { missingProperty: string }).missingProperty;
return `${label} está sem o campo obrigatório ${missing}`;
}
if (errorItem.keyword === 'minLength') {
return `${label} é curto demais`;
}
return `${label} falhou na validação (${errorItem.keyword})`;
});
}
No lado da API, mantenha uma estrutura legível por máquina para que o frontend decida como apresentá-la:
{
"type": "https://bbdrug.com/problems/validation",
"title": "Request data validation failed",
"status": 422,
"errors": [
{ "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
]
}
7. Desempenho e armadilhas
- O custo de validação cresce com a complexidade do schema e o tamanho dos dados; múltiplos ramos
oneOfcausam explosão combinatória, o que fica muito perceptível com milhares de registros; uniqueItemsfaz uma comparação profunda O(n²) sobre o array; para arrays grandes, deduplique na camada de negócio;- Evite padrões de backtracking catastrófico em
pattern(como(a+)+$); este é o ponto de entrada clássico para ReDoS; - Quando
additionalProperties: falseé combinado comallOf, campos declarados em um sub-schema são julgados "adicionais" pelo pai, então você precisa mesclá-los em um único schema ou usarunevaluatedProperties.
8. Lista de verificação
- [ ] Fixe 2020-12 e declare
$schemano topo do arquivo; - [ ] Extraia estruturas compartilhadas com
$defs+$refem vez de copiar e colar; - [ ] Prefira discriminadores a
oneOf; - [ ] Reutilize o resultado de
compiledo validador e habilite o modo estrito; - [ ] Documente claramente se
formaté imposto; - [ ] As respostas de erro fornecem o caminho, a palavra-chave e uma mensagem voltada ao usuário;
- [ ] Coloque o schema no CI: rode os casos do JSON Schema Test Suite como testes de regressão.
Perguntas frequentes
Como devo escolher um draft de JSON Schema?
Use 2020-12 para projetos novos e declare $schema no topo do arquivo. Se o schema entra em OpenAPI 3.1, ele também precisa ser 2020-12, porque a 3.1 embute essa versão diretamente; mantenha o draft-07 apenas para projetos ainda presos a cadeias de ferramentas mais antigas. Os dois se comportam de forma diferente para items e para palavras-chave irmãs de $ref, então não os misture em um conjunto de schemas.
Por que additionalProperties: false amplia problemas de compatibilidade?
Ele trata toda chave não declarada como erro, então um campo extra de um cliente falha o payload inteiro. Todo chamador então precisa atualizar em sintonia sempre que a API evolui, ou clientes antigos são rejeitados. Schemas no estilo de configuração devem fechar campos extras explicitamente, enquanto APIs públicas precisam pesar o custo de compatibilidade.
Devo usar oneOf ou anyOf?
Use oneOf apenas quando os dados precisarem corresponder a exatamente um ramo. Se as restrições dos ramos puderem valer ao mesmo tempo, um registro corresponde a vários ramos e oneOf falha de vez; mude para anyOf, ou acrescente um discriminador const dentro de cada ramo.