JSON Toolkit

Pontos-chave para projetar contratos de dados de API JSON

Resumo: 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 leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de Pontos-chave para projetar contratos de dados de API JSON

Resumo da ópera: trave o envelope de resposta, o formato de erro, a representação de ID e tempo e a semântica de null versus ausente no contrato antes de discutir detalhes de paginação e nomeação; sempre envolva o nível superior em um envelope data para que paginação e IDs de rastreio possam ser adicionados depois sem quebrar chamadores existentes.

Não há respostas padrão em projeto de API, mas há algumas decisões "difíceis de retomar depois de escolhidas" que é melhor pensar antes de escrever a primeira linha de código.

1. Envolver a resposta ou não

Três estilos:

// Estilo A: resposta crua
{ "id": 1, "name": "张三" }

// Estilo B: envelope de dados
{ "data": { "id": 1, "name": "张三" } }

// Estilo C: dados + metadados
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Estilo Prós Contras
Resposta crua Mais simples, uma camada a menos para o frontend Não consegue anexar metadados; um array de nível superior carrega riscos de JSON hijacking e de evolução
Envelope de dados Fácil adicionar meta / errors / links Um nível de aninhamento a mais, o frontend precisa escrever res.data.data
Dados + metadados Melhor observabilidade Exige acordo estrito sobre quais campos pertencem a meta

Recomendação: use um envelope data uniformemente para listas e detalhes, e nunca faça o nível superior ser um array. Assim, adicionar paginação ou IDs de rastreio depois nunca quebra o contrato.

2. Formato de erro: copie a RFC 9457

Inventar sua própria estrutura de erro é uma enorme fonte de custo de colaboração. A RFC 9457 (que substitui a RFC 7807) define application/problem+json:

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
Campo Significado Obrigatório
type URI estável do tipo de problema Recomendado
title Resumo curto legível por humanos Recomendado
status Código de status HTTP Recomendado
detail Explicação específica do que aconteceu desta vez Opcional
instance A instância de recurso que teve o problema Opcional
Campos de extensão Campos de negócio arbitrários Opcional

Campos de extensão (como balance / required acima) são permitidos pela especificação, e o frontend pode usá-los para mensagens precisas em vez de analisar o texto de detail.

3. Paginação: cursor vence offset

// Paginação por offset
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Paginação por cursor
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Dimensão Paginação por offset Paginação por cursor
Dificuldade de implementação Baixa (LIMIT/OFFSET) Média (exige uma chave de ordenação estável)
Desempenho em páginas profundas Ruim (quanto maior o offset, mais lento) Bom
Quando os dados mudam Duplicatas ou itens ausentes Estável
Pular para uma página Possível Não possível
Precisa de uma contagem total Exige uma contagem extra Não exigida

Recomendação: use paginação por offset em tabelas administrativas (elas precisam de saltos de página e totais), e paginação por cursor em feeds infinitos voltados ao usuário.

4. IDs e tempo

4.1 Sempre use strings para IDs

O raciocínio foi coberto no artigo sobre tipos: no lado JavaScript, valores acima de 2^53 perdem precisão silenciosamente. Mesmo que o ID atual seja um inteiro autoincremental pequeno, usar strings deixa espaço para mudar para IDs snowflake depois.

{ "id": "1000000000000000001" }

4.2 Use strings RFC 3339 para tempo

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • Não use números de timestamp em milissegundos: eles são difíceis de ler, e a unidade (segundos/milissegundos) é frequentemente confundida;
  • Especifique o fuso horário explicitamente: ou com Z, ou com um offset; nunca use "hora local sem fuso horário";
  • Nomeie os campos uniformemente como xxxAt para que fique óbvio à primeira vista que é um ponto no tempo.

5. null e ausente

Você precisa escolher uma e escrever na documentação:

Abordagem Expressar "sem valor" Expressar "não modificar"
Abordagem A O valor do campo é null Omita o campo na requisição
Abordagem B O campo não existe Omita o campo na requisição

O problema da abordagem A é que "omitido" e "enviado como null" significam coisas diferentes em um PATCH, o que é fácil de mal interpretar; o problema da abordagem B é que o JSON.stringify do frontend descarta campos cujo valor é undefined, o que por acaso corresponde à semântica.

Recomendação prática:

  • Use a abordagem A para corpos de resposta (null claramente significa sem valor, e o frontend não precisa distinguir undefined);
  • Use a abordagem B para corpos de requisição (especialmente PATCH), combinada com Merge Patch ou JSON Patch para tornar a semântica explícita.

6. Nomeação e estrutura de campos

Decisão Escolhas comuns Cuidado
Estilo de nomeação camelCase (ecossistema JS) / snake_case (Python, bancos de dados) Uniforme em todo o projeto, não misture
Valores de enum Minúsculas com underscores, "pending_review" Não use números; são difíceis de ler
Campos booleanos isActive / enabled Não use status: 0/1
Campos de array Substantivos no plural, items Retorne [] para arrays vazios, nunca null
Profundidade de aninhamento No máximo 3 níveis Mais profundo que isso, divida ou ofereça um parâmetro expand

Retornar [] em vez de null para arrays vazios importa muito: data.items.map(...) no frontend não quebrará por causa de um null.

7. Estratégia de evolução

Mudança Quebra compatibilidade Como lidar
Adicionar um campo opcional Não Publique diretamente
Adicionar um campo obrigatório Sim Adicione um valor padrão, ou publique por versão
Remover um campo Sim Marque como obsoleto primeiro, observe os chamadores e depois remova
Mudar o tipo de um campo Sim Adicione um novo nome de campo e grave em duplicidade durante a transição
Mudar o conjunto de valores de enum Depende O cliente precisa ter fallback para valores desconhecidos
Mudar a semântica de um campo Sim Trate como um novo campo

Três regras de disciplina:

  1. Clientes precisam ser tolerantes a campos desconhecidos (não se prenda com additionalProperties: false);
  2. Clientes precisam ter um ramo de fallback para valores de enum desconhecidos;
  3. Mantenha campos obsoletos por pelo menos uma versão principal, e marque-os com Deprecation nos cabeçalhos de resposta ou na documentação.

8. Um exemplo completo

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

Esta resposta reflete todas as convenções acima: envelope data, IDs como strings, valores monetários como strings, timestamps com fuso horário, null significando ausente, array vazio, paginação por cursor e meta para informações de observabilidade.

9. Lista de verificação

  • [ ] O nível superior não é um array; use um envelope data uniformemente;
  • [ ] As respostas de erro seguem a RFC 9457 e incluem um type estável;
  • [ ] O formato de paginação corresponde ao cenário de negócio;
  • [ ] IDs são strings, e o tempo usa RFC 3339;
  • [ ] A semântica de null e ausente está escrita na documentação;
  • [ ] O estilo de nomeação é uniforme em todo o projeto, e os enums usam strings em minúsculas;
  • [ ] O cliente tem fallbacks para campos desconhecidos e valores de enum desconhecidos;
  • [ ] Mudanças disruptivas passam por versionamento ou por uma transição com gravação dupla.

Perguntas frequentes

Como devo projetar uma API JSON evolutiva?

Envolva listas e detalhes uniformemente em um envelope data e nunca faça o nível superior ser um array, para que paginação e IDs de rastreio possam ser adicionados sem quebrar o contrato. Use IDs como strings e timestamps RFC 3339, e escreva a semântica de null versus ausente na documentação. Mantenha campos novos opcionais e semanticamente independentes, e dê aos clientes fallbacks para campos e valores de enum desconhecidos.

Por que não inventar um formato de erro personalizado?

Inventar sua própria estrutura de erro é uma enorme fonte de custo de colaboração. Copie o application/problem+json da RFC 9457, identifique problemas com um type estável e permita campos de extensão como balance e required, para que o frontend consiga construir mensagens precisas sem analisar o texto de detail.

Todo endpoint deve usar paginação por cursor?

Não. Tabelas administrativas precisam de saltos de página e totais, então a paginação por offset encaixa melhor; feeds infinitos voltados ao usuário devem usar paginação por cursor, que permanece estável em páginas profundas e evita duplicatas ou lacunas quando os dados mudam. Escolha com base em o cenário precisar de saltos de página e totais.