JSON Toolkit

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.

Intermediário~14 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de JSON Schema: do básico à produção

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:

  1. required só exige que a chave exista: {"name": ""} é válido. Para rejeitar uma string vazia você precisa acrescentar minLength: 1;
  2. additionalProperties: false amplia 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;
  3. properties nã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 usa definitions;
  • 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 $ref com palavras-chave irmãs varia por versão). Quando precisar de composição, use allOf.

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 compile uma ú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: true revela cedo palavras-chave mal escritas (por exemplo minlength em vez de minLength), então vale habilitar;
  • Só ajv-formats faz format: "email" e format: "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 oneOf causam explosão combinatória, o que fica muito perceptível com milhares de registros;
  • uniqueItems faz 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 com allOf, campos declarados em um sub-schema são julgados "adicionais" pelo pai, então você precisa mesclá-los em um único schema ou usar unevaluatedProperties.

8. Lista de verificação

  • [ ] Fixe 2020-12 e declare $schema no topo do arquivo;
  • [ ] Extraia estruturas compartilhadas com $defs + $ref em vez de copiar e colar;
  • [ ] Prefira discriminadores a oneOf;
  • [ ] Reutilize o resultado de compile do 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.

Referências

Artigos relacionados

Sincronização bidirecional entre Schema e tipos TypeScript

Tipos e schemas cobrem cada um um segmento: um em tempo de compilação, outro em tempo de execução. A questão não é qual escolher, mas como manter os dois sincronizados automaticamente.

Intermediário~11 min de leituraAtualizado em 2026-10-30

Pontos-chave para projetar contratos de dados de API JSON

Seis coisas são muito difíceis de mudar depois de publicadas: envelopes de resposta, formatos de erro, formato de paginação, representação de IDs e tempo, a semântica de null versus ausente e a evolução de versão.

Intermediário~13 min de leituraAtualizado em 2026-10-30

Sem additionalProperties, a validação de schema não valida nada

Validação bem-sucedida não significa dados limpos. O Pydantic ignora campos desconhecidos por padrão, o Zod os remove e o Ajv deixa passar — parâmetros alucinados e chaves com erro de digitação atravessam em silêncio. Este artigo mostra as três formas de aceitação silenciosa e dá receitas completas: fechar objetos, controlar coerção e apertar cada nível de aninhamento.

Intermediate~8 min de leituraAtualizado em 2026-11-02