Boas Práticas de JSON em APIs REST: Os 7 Erros Que Quebram Integrações
← Voltar para Codeshort

Boas Práticas de JSON em APIs REST: Os 7 Erros Que Quebram Integrações

Tipos inconsistentes, datas sem padrão, null usado errado — erros de JSON são silenciosos até virarem incidente em produção. Guia completo com validação por schema, ferramentas e checklist.

DC
Dev Code Software
23 de março de 2026·10 min de leitura

Índice


Por que JSON "funcional" não é o mesmo que JSON correto

Duas horas tentando entender por que o frontend quebrava em produção. O campo id vinha como string em alguns registros e como number em outros — mesmo endpoint, mesma API. Era um serviço legado que ninguém tinha tocado em meses e que ninguém tinha validado nunca.

O problema não era o código. Era o JSON.

Quando falamos em "formatar JSON corretamente", a maioria pensa em indentação. Mas indentação é o menor dos problemas. Formatar JSON corretamente significa:

  • Tipos consistentes: mesmo campo tem o mesmo tipo em todos os objetos
  • Semântica clara: null usado com intenção, campos ausentes quando não se aplicam
  • Convenção única de nomes, do primeiro ao último campo
  • Datas em formato universal, não em "DD/MM/YYYY que todo mundo entende"
  • Arrays com elementos do mesmo formato — sempre

Esses erros não aparecem no console.log. Aparecem quando o TypeScript infere id: number | string e você passa três horas entendendo por quê. Aparecem quando o app de pagamentos do parceiro rejeita sua data porque ele esperava ISO 8601. Aparecem em produção, às 23h, depois de um deploy que "só mudou uma coisa pequena".

O que você vai encontrar aqui:

  • 7 erros concretos com exemplos de como corrigem bugs reais
  • Validação automática com JSON Schema e ajv
  • Exemplos práticos de jq no terminal (vale o scroll até lá)
  • Checklist completo para revisar antes de colocar uma API em produção

Os 7 erros de JSON que mais causam bugs em produção

Erro 1: Misturar tipos no mesmo campo

❌ O campo "id" deveria ser sempre o mesmo tipo
[
  { "id": 1,   "nome": "Ana"    },
  { "id": "2", "nome": "Carlos" }
]

✅ Um tipo, em todos os objetos, sem exceção
[
  { "id": 1, "nome": "Ana"    },
  { "id": 2, "nome": "Carlos" }
]

Isso quebra deserializadores tipados, validações de schema e qualquer ORM que mapeia os dados. No TypeScript, a inferência resulta em id: number | string — e a partir daí você está lutando contra o compilador em cada lugar que usa aquele campo.

O cenário mais comum: um campo que começa como integer no banco de dados mas é serializado como string por um endpoint legado feito às pressas. Seis meses depois, outro endpoint serializa corretamente como integer. Dois padrões, mesmo campo, nenhuma documentação.


Erro 2: Usar null quando o campo simplesmente não existe

❌ Campos inaplicáveis como null geram ruído desnecessário
{
  "nome": "Pedro",
  "telefone": null,
  "endereco": null,
  "faturamento": null
}

✅ Omita o campo quando ele não se aplica ao contexto
{
  "nome": "Pedro"
}

null tem semântica específica: o campo existe, foi processado, e o valor é intencionalmente vazio. Isso é diferente de "esse campo não se aplica a este objeto".

Quando você manda "telefone": null, está dizendo que Pedro tem um campo telefone e que ele está vazio — talvez porque ele não quis informar, talvez porque foi apagado. Quem consome pode tomar decisões baseadas nisso: exibir um placeholder, mostrar um botão "adicionar telefone", ou disparar uma validação.

Se o campo simplesmente não existe no contexto daquele objeto, omitir é a resposta semanticamente correta — e evita lógica condicional desnecessária no consumidor.

Regra prática: use null quando o campo faz parte do schema mas o valor está ausente por escolha ou processo. Omita quando o campo não pertence ao contexto daquele objeto.


Erro 3: Misturar camelCase e snake_case no mesmo objeto

❌ Três convenções diferentes no mesmo payload
{
  "userId": 42,
  "user_name": "joao",
  "UserEmail": "joao@email.com"
}

✅ Uma convenção, sem exceção
{
  "userId": 42,
  "userName": "joao",
  "userEmail": "joao@email.com"
}

Para APIs REST consumidas por JavaScript ou TypeScript: camelCase. Se o banco usa snake_case (e bancos relacionais geralmente usam), você normaliza na camada de serialização — o modelo do banco não deve vazar para a API pública.

O que não pode acontecer em nenhuma hipótese é misturar os dois no mesmo objeto. Isso força quem consome a conhecer a origem de cada campo para saber qual convenção usar — o que é o oposto de uma API bem projetada.


Erro 4: Datas sem formato padronizado

❌ Cada campo de data num formato diferente — armadilha garantida
{
  "criado_em": "23/03/2026",
  "atualizado_em": "2026-03-23T10:00:00",
  "expira": "March 23, 2026"
}

✅ ISO 8601 com timezone explícito — funciona em qualquer linguagem
{
  "criadoEm": "2026-03-23T10:00:00Z",
  "atualizadoEm": "2026-03-23T12:30:00Z",
  "expiraEm": "2026-04-23T00:00:00Z"
}

ISO 8601 (YYYY-MM-DDTHH:mm:ssZ) é parseado nativamente por Date.parse() no JavaScript, datetime.fromisoformat() no Python, Instant.parse() no Java — sem configuração extra.

DD/MM/YYYY é armadilha pura: o campo "03/04/2026" é 3 de abril ou 4 de março? Depende do locale de quem lê. Já vi bug de expiração de token causado exatamente por isso — um serviço parseava como mês/dia, outro como dia/mês, e os tokens duravam até uma data errada sem nenhum erro explícito.

O Z no final significa UTC. Sempre use UTC no backend e converta para o fuso do usuário no frontend. Nunca armazene datas em fuso local.


Erro 5: Arrays com elementos de tipos diferentes

❌ Array misturando tipos — impossível de tipar no consumidor
{
  "itens": [1, "dois", { "valor": 3 }, null]
}

✅ Arrays com elementos do mesmo formato
{
  "itens": [
    { "id": 1, "descricao": "um"  },
    { "id": 2, "descricao": "dois" },
    { "id": 3, "descricao": "tres" }
  ]
}

Ninguém escreve isso de propósito. Acontece quando dois serviços diferentes começam a alimentar o mesmo array sem contrato definido, ou quando um endpoint antigo retorna IDs como números e um novo retorna como objetos. O TypeScript infere o tipo como (number | string | { valor: number } | null)[] — inútil para trabalhar.

Valide na borda. Se o array vem de uma fonte externa que você não controla completamente, normalize antes de processar.


Erro 6: Caracteres especiais sem encoding correto

❌ Strings com caracteres não escapados podem quebrar parsers
{
  "descricao": "Produto "especial" com desconto",
  "caminho": "C:\Users\joao\arquivo.txt"
}

✅ Use JSON.stringify — ele cuida do escaping automaticamente
{
  "descricao": "Produto \"especial\" com desconto",
  "caminho": "C:\\Users\\joao\\arquivo.txt"
}

Esse erro quase nunca aparece quando você usa JSON.stringify corretamente — aparece quando alguém constrói JSON como concatenação de strings (veja o Erro 7) ou quando um sistema legado serializa manualmente. O resultado é um JSON que parece válido visualmente mas quebra em JSON.parse().

Caracteres que precisam de escape em JSON: "\", \\\, quebra de linha → \n, tab → \t.


Erro 7: Construir JSON por concatenação de strings

// ❌ Nunca construa JSON manualmente como string
const nome = req.body.nome; // e se vier: João "da Silva"?
const json = '{"nome": "' + nome + '", "id": ' + id + '}';
// Resultado: {"nome": "João "da Silva"", "id": 42}  ← JSON inválido

// ✅ JSON.stringify escapa os valores corretamente
const json = JSON.stringify({ nome, id });

// ✅ Com indentação legível para logs e debug
const jsonFormatado = JSON.stringify({ nome, id }, null, 2);

Concatenar strings para montar JSON é o caminho mais curto para JSON inválido, injeção de dados e bugs impossíveis de rastrear. Se o campo contiver aspas, barras invertidas ou quebras de linha, a string resultante não é JSON válido — e você só vai descobrir quando o JSON.parse explodir no consumidor.

JSON.stringify resolve isso de forma transparente. Não tem nenhuma razão para não usá-lo.


Como validar JSON com JSON Schema no Node.js

Para APIs que trocam JSON entre sistemas, validar a estrutura na borda — antes de processar — evita que dados malformados cheguem ao banco de dados ou causem erros internos difíceis de diagnosticar.

JSON Schema é o padrão mais adotado para isso. A biblioteca ajv é a implementação mais rápida disponível para Node.js.

Instalação

npm install ajv ajv-formats

Validação básica com ajv

import Ajv from "ajv";
import addFormats from "ajv-formats";

const ajv = new Ajv({ allErrors: true }); // allErrors: retorna todos os erros, não só o primeiro
addFormats(ajv);

const schemaUsuario = {
  type: "object",
  properties: {
    id:       { type: "integer", minimum: 1 },
    nome:     { type: "string", minLength: 1, maxLength: 100 },
    email:    { type: "string", format: "email" },
    criadoEm: { type: "string", format: "date-time" },
    role:     { type: "string", enum: ["admin", "user", "guest"] },
  },
  required: ["id", "nome", "email"],
  additionalProperties: false, // rejeita campos não declarados no schema
};

const validarUsuario = ajv.compile(schemaUsuario);

function processarUsuario(dados) {
  if (!validarUsuario(dados)) {
    // ajv.errorsText formata os erros de forma legível
    const erros = ajv.errorsText(validarUsuario.errors);
    throw new Error(`Payload inválido: ${erros}`);
  }

  // A partir daqui, você tem garantia de que os tipos estão corretos
  return dados;
}

Validando datas e formatos especiais

// ajv-formats adiciona suporte a: date, date-time, email, uri, uuid, ipv4 e outros
const schemaEvento = {
  type: "object",
  properties: {
    id:         { type: "string", format: "uuid" },
    titulo:     { type: "string" },
    inicioEm:   { type: "string", format: "date-time" },
    fimEm:      { type: "string", format: "date-time" },
    urlExterno: { type: "string", format: "uri" },
  },
  required: ["id", "titulo", "inicioEm"],
};

Integração com um endpoint Next.js

// app/api/usuarios/route.ts
import Ajv from "ajv";
import addFormats from "ajv-formats";

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

const schema = {
  type: "object",
  properties: {
    nome:  { type: "string", minLength: 1 },
    email: { type: "string", format: "email" },
    role:  { type: "string", enum: ["admin", "user"] },
  },
  required: ["nome", "email"],
  additionalProperties: false,
};

const validar = ajv.compile(schema);

export async function POST(req: Request) {
  const body = await req.json();

  if (!validar(body)) {
    return Response.json(
      { error: "Payload inválido", detalhes: validar.errors },
      { status: 400 }
    );
  }

  // body está validado e tipado aqui
  const usuario = await db.user.create({ data: body });
  return Response.json(usuario, { status: 201 });
}

Uma ressalva honesta: additionalProperties: false é muito útil para APIs públicas onde você não controla quem chama. Para APIs internas onde os dois lados são seus, considere apenas logar campos inesperados em vez de rejeitar — reduz fricção durante iterações rápidas. Depende do quanto você controla a integração.

Alternativa com TypeScript: se o projeto já usa TypeScript, zod ou valibot oferecem validação com inferência de tipos — você valida e já ganha o tipo correto sem precisar escrever interface separada:

import { z } from "zod";

const UsuarioSchema = z.object({
  id:    z.number().int().positive(),
  nome:  z.string().min(1),
  email: z.string().email(),
});

type Usuario = z.infer<typeof UsuarioSchema>; // tipo gerado automaticamente

const resultado = UsuarioSchema.safeParse(body);
if (!resultado.success) {
  return Response.json({ error: resultado.error.format() }, { status: 400 });
}
// resultado.data é tipado como Usuario

JSON.stringify vs concatenação de strings

Já coberto no Erro 7, mas vale repetir o ponto sobre casos menos óbvios:

// Caso não óbvio: serialização em logs
// ❌ Pode gerar log inválido se o objeto contém valores circulares
console.log("Dados: " + objeto);           // "[object Object]"
console.log(`Dados: ${JSON.stringify(objeto)}`); // ✅ JSON válido

// Caso não óbvio: comparação de objetos
// ❌ Nunca compare objetos serializados assim para lógica de negócio
if (JSON.stringify(a) === JSON.stringify(b)) { ... }
// A ordem das chaves afeta o resultado — use uma lib de deep-equal

// Caso útil: clonar objeto simples (sem funções, datas ou undefined)
const clone = JSON.parse(JSON.stringify(objeto));
// Para objetos com Date, use structuredClone() que está disponível no Node 17+
const cloneCorreto = structuredClone(objeto);

jq: a ferramenta que você está ignorando no terminal

Aprendi jq tarde e me arrependo de não ter aprendido antes. É um processador de JSON para terminal — mas na prática é uma linguagem de query para JSON, e em 5 minutos você consegue fazer coisas que levariam 20 linhas de JavaScript.

Instalação

# macOS
brew install jq

# Ubuntu/Debian
sudo apt install jq

Exemplos práticos que uso toda semana

# Ver JSON formatado de uma API (o uso mais básico)
curl -s https://api.exemplo.com/usuarios | jq .

# Extrair só um campo de cada objeto num array
curl -s https://api.exemplo.com/usuarios | jq '.[].email'

# Filtrar objetos por condição
curl -s https://api.exemplo.com/usuarios | jq '.[] | select(.role == "admin")'

# Criar novo objeto com só os campos que interessam
curl -s https://api.exemplo.com/usuarios | jq '.[] | { id: .id, nome: .nome }'

# Contar itens num array
curl -s https://api.exemplo.com/usuarios | jq '. | length'

# Ordenar por campo
curl -s https://api.exemplo.com/produtos | jq 'sort_by(.preco)'

# Trabalhar com um arquivo local
cat resposta.json | jq '.data.itens[] | select(.status == "ativo") | .id'

# Validar JSON de um arquivo (retorna erro se inválido)
cat arquivo.json | python3 -m json.tool > /dev/null && echo "válido" || echo "inválido"

jq é especialmente útil durante debug de integrações: você recebe um payload gigante, quer inspecionar só uma parte, e não quer escrever um script JavaScript inteiro para isso.


Outras ferramentas essenciais

SituaçãoFerramentaPor que usar
Validar JSON no terminalpython3 -m json.toolNativo, sem instalação
Query e transformação no terminaljqExtremamente poderoso para scripts
Validar schema em Node.jsajv + ajv-formatsMais rápido do ecossistema
Validar + tipar em TypeScriptzod ou valibotInferência de tipo automática
Inspecionar APIs no browserJSON Viewer (extensão)Formata automaticamente no browser
Gerar schema a partir de dadosquicktype.ioGera tipos TypeScript, Go, Python, etc.
Transformar e filtrar dadosjq no terminalSubstitui scripts de transformação simples

Checklist de JSON para produção

Use antes de publicar um endpoint novo ou revisar uma API existente:

ItemO que verificar
Tipos consistentesO mesmo campo tem o mesmo tipo em todos os objetos da coleção?
null com intençãonull indica campo ausente por escolha? Se não se aplica, o campo está omitido?
Convenção de nomescamelCase ou snake_case — apenas um, em todos os campos?
Datas em ISO 8601Todas as datas usam YYYY-MM-DDTHH:mm:ssZ? Timezone explícito?
Arrays homogêneosTodos os elementos do array têm o mesmo formato/tipo?
Encoding corretoStrings com aspas e barras foram geradas via JSON.stringify, não concatenação?
Schema validadoO endpoint valida o payload de entrada antes de processar?
Campos sensíveisO payload não expõe passwordHash, tokens, dados de pagamento ou PII desnecessários?
additionalPropertiesAPIs públicas rejeitam ou logam campos inesperados?
Erros com estrutura consistenteRespostas de erro seguem o mesmo formato { error, code, detalhes }?

FAQ — Perguntas frequentes sobre JSON em APIs

Qual o formato correto de data em JSON?

ISO 8601 com timezone UTC: "2026-03-23T10:00:00Z". O Z significa UTC. É o único formato que qualquer linguagem moderna parseia nativamente sem configuração extra. Nunca use DD/MM/YYYY — a ordem de dia e mês é ambígua para sistemas de outros países.


O que é JSON Schema e quando devo usar?

JSON Schema é um vocabulário para descrever a estrutura esperada de um JSON — quais campos existem, quais são obrigatórios, quais tipos cada um aceita. Use quando sua API recebe dados de sistemas externos que você não controla completamente, ou quando a consistência do payload é crítica (ex: integração com pagamentos, webhooks de terceiros). Para APIs internas com TypeScript, zod faz o mesmo trabalho com melhor integração de tipos.


camelCase ou snake_case em APIs REST?

camelCase é o padrão de facto para APIs REST consumidas por JavaScript/TypeScript. A maioria dos bancos de dados relacionais usa snake_case internamente — normalize na camada de serialização, não exponha o modelo do banco na API. O que importa é consistência: escolha um e não misture.


JSON é o formato certo para minha API ou devo usar outra coisa?

JSON é a escolha certa para a maioria das APIs REST. As alternativas mais consideradas são: Protocol Buffers (Protobuf) para APIs com requisitos de alta performance e baixo payload (gRPC), MessagePack para serialização binária mais compacta que JSON, e CBOR para IoT e contextos com largura de banda muito limitada. Para uma API web convencional, JSON com validação adequada é suficiente.


Como debugar um JSON que está quebrando o JSON.parse?

Primeiro, tente python3 -m json.tool < arquivo.json no terminal — ele aponta a linha exata do erro. Se for uma string recebida em runtime, use um bloco try/catch em volta do JSON.parse e logue o conteúdo cru antes de parsear. Erros comuns: vírgula sobrando no último elemento, aspas não escapadas dentro de strings, undefined serializado (JavaScript converte undefined para null em JSON.stringify, mas alguns sistemas não).


Quando devo omitir um campo vs. enviar null?

Omita quando o campo não pertence ao conceito daquele objeto naquele contexto — ex: um usuário sem perfil empresarial simplesmente não tem cnpj. Use null quando o campo faz parte do schema mas o valor está ausente por escolha ou processo — ex: "dataExclusao": null indica que o registro ainda está ativo. A diferença é semântica: null diz "existe mas está vazio", campo ausente diz "não se aplica".


Se a API que você mantém ainda não valida o JSON de entrada antes de processar, esse é o próximo passo — e provavelmente o com melhor custo-benefício em termos de bugs evitados por hora de trabalho.