JSON.parse terminou, mas o valor retornado ainda não é um User. Ele é apenas um valor JavaScript que veio de fora do processo. Uma interface TypeScript descreve o que o código espera receber; ela não inspeciona uma resposta HTTP, um arquivo ou uma mensagem de fila.

A fronteira segura é pequena e explícita: confirmar o status da resposta, analisar o texto, validar o formato e só então entregar um valor tipado à regra de negócio. O padrão abaixo não depende de biblioteca. Depois, você pode trocar o guard por Zod, Valibot, JSON Schema ou um cliente gerado sem mudar a ideia central.

Esse mesmo cuidado aparece ao validar argumentos antes de executar uma tool. A origem muda, mas o contrato é igual: dado externo precisa ser verificado antes de produzir efeitos.

Fluxo de validação de JSON externo até um valor tipado

Neste guia, você vai montar um guard para um objeto aninhado, conectá-lo a fetch, testar entradas quebradas e separar o que a validação de formato realmente garante.

TypeScript consegue validar JSON em runtime?

Não. O compilador pode ajudar a estreitar um valor depois que uma condição foi executada, mas não observa o conteúdo recebido pela rede. A documentação TypeScript for the New Programmer explica que os tipos são removidos quando o código é transformado, e a referência de Basic Types alerta que uma asserção de tipo não faz verificação em runtime.

Este código compila, mas não valida nada:

type User = {
  id: string;
  name: string;
};

const user = JSON.parse(body) as User;
console.log(user.name.toUpperCase());

Se body contiver { "id": 42 }, a asserção não corrige o número nem percebe a ausência de name. Ela apenas instrui o compilador a aceitar a sua promessa. Para representar corretamente a entrada, comece com unknown e faça o estreitamento em runtime, como mostra o Handbook de Narrowing do TypeScript.

O ponto importante não é escolher entre um guard manual e uma biblioteca. É definir o contrato na entrada e fazer o restante do programa receber somente o resultado que passou por esse contrato. A ferramenta pode mudar; essa fronteira continua sendo responsabilidade do código.

Qual é a diferença entre analisar JSON e validar seu formato?

São falhas diferentes. Analisar converte texto JSON válido em um valor JavaScript. Validar formato verifica se esse valor tem os campos e tipos que o programa precisa. JSON.parse lança um SyntaxError para texto inválido, conforme a referência da MDN sobre JSON.parse, mas um JSON sintaticamente correto pode ter qualquer formato.

Faça a análise retornar unknown e mantenha essa etapa sem a pretensão de validar o domínio:

function parseJson(text: string): unknown {
  try {
    return JSON.parse(text) as unknown;
  } catch (error) {
    if (error instanceof SyntaxError) {
      throw new Error("Resposta não contém JSON válido", { cause: error });
    }

    throw error;
  }
}

O as unknown não adiciona proteção. Ele torna a intenção visível: o parser terminou, e agora o valor ainda precisa passar por outra função. Essa separação também deixa os testes mais precisos, porque uma entrada pode falhar por sintaxe ou por formato.

Como escrever um guard para um objeto aninhado?

Um guard é uma função que retorna um predicado de tipo, como value is User, depois de verificar propriedades em runtime. O TypeScript documenta type predicates e narrowing como a forma de ensinar ao compilador o que uma condição realmente estabeleceu.

O exemplo seguinte valida um usuário, incluindo um campo aninhado e uma lista. Ele rejeita valores que não correspondem ao contrato e devolve User somente no caminho aprovado:

type User = {
  id: string;
  profile: {
    name: string;
  };
  active: boolean;
  tags: string[];
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function readUser(value: unknown): User {
  if (!isRecord(value)) {
    throw new Error("Usuário precisa ser um objeto");
  }

  const profile = value.profile;
  const tags = value.tags;

  if (
    typeof value.id !== "string" ||
    !isRecord(profile) ||
    typeof profile.name !== "string" ||
    typeof value.active !== "boolean" ||
    !Array.isArray(tags) ||
    !tags.every((tag) => typeof tag === "string")
  ) {
    throw new Error("Formato de usuário inválido");
  }

  return {
    id: value.id,
    profile: { name: profile.name },
    active: value.active,
    tags,
  };
}

Há uma escolha deliberada no retorno: ele cria um novo objeto com os campos conhecidos. Assim, campos extras do payload não viram parte acidental do contrato interno. Se a aplicação precisar preservá-los, essa decisão deve aparecer no tipo e nos testes, em vez de acontecer por acidente.

Este artigo não apresenta um caso de cliente nem um benchmark próprio. O valor do exemplo é ser pequeno, executável e fácil de adaptar para o contrato real da aplicação.

Como ligar o guard a uma resposta HTTP?

Uma resposta HTTP tem pelo menos duas dimensões relevantes: o resultado do transporte e o conteúdo. No fetch do Node.js, Response e seus globais expõem ok, status e métodos para ler o corpo. Um status de sucesso não prova que o JSON tem o formato esperado.

Junte as etapas sem transformar uma falha em outra:

async function fetchUser(url: string): Promise<User> {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`Serviço de usuários respondeu ${response.status}`);
  }

  const body = await response.text();
  return readUser(parseJson(body));
}

A chamada agora falha em lugares observáveis: o serviço pode responder com erro HTTP, o corpo pode não ser JSON, ou o JSON pode não corresponder ao contrato. Para endpoints que recebem chamadas de terceiros, vale também testar o contrato real de uma entrega HTTP, incluindo assinaturas, duplicatas e reentregas.

Não esconda o status dentro do guard. readUser deve saber sobre dados; fetchUser deve saber sobre transporte. Essa divisão mantém a função de formato reutilizável para arquivos, filas e respostas já lidas.

Como testar a fronteira sem confiar no caminho feliz?

Teste o contrato onde a entrada chega. A recomendação da OWASP sobre validação de entrada favorece validação explícita e restritiva quando o dado entra no sistema, mas essa etapa não substitui autenticação, autorização ou controles específicos da operação.

Com um runner de testes qualquer, a ideia mínima é cobrir sintaxe, formato, aninhamento, arrays e sucesso:

const validBody = JSON.stringify({
  id: "u_123",
  profile: { name: "Ada" },
  active: true,
  tags: ["admin", "billing"],
});

if (readUser(parseJson(validBody)).profile.name !== "Ada") {
  throw new Error("O payload válido deveria passar");
}

function assertRejects(invalidBody: string): void {
  let rejected = false;

  try {
    readUser(parseJson(invalidBody));
  } catch {
    rejected = true;
  }

  if (!rejected) {
    throw new Error("A entrada inválida passou");
  }
}

for (const invalidBody of [
  "{",
  "null",
  JSON.stringify({ id: "u_123", profile: { name: "Ada" } }),
  JSON.stringify({
    id: "u_123",
    profile: { name: "Ada" },
    active: true,
    tags: ["admin", 7],
  }),
]) {
  assertRejects(invalidBody);
}

Em um projeto real, acrescente casos para campos opcionais, null, arrays vazios, valores extremos e mudanças de versão do fornecedor. Também teste o corpo de um status HTTP de erro quando a camada de transporte fizer parte do contrato.

Uma boa mensagem de falha preserva o ponto onde a confiança acabou. Dizer "JSON inválido", "status 404" e "campo active ausente" é mais útil para diagnóstico do que converter todos os caminhos em "não foi possível carregar usuário".

Quando vale trocar o guard por uma biblioteca de schema?

Um guard manual pode ser suficiente para um contrato pequeno e estável. Uma biblioteca de schema passa a fazer sentido quando vários endpoints compartilham regras, quando mensagens de erro precisam ser estruturadas ou quando o schema também alimenta documentação e geração de tipos. JSON Schema e clientes gerados podem ser melhores quando existe um contrato formal mantido por mais de uma equipe.

O critério não é eliminar código manual a qualquer custo. É manter uma única fonte de verdade e garantir que a validação aconteça no limite correto. Em uma aplicação com ferramentas, a mesma decisão aparece ao validar argumentos e saídas de uma tool. Para configurações locais, veja como separar tipos de validação em runtime, porque configuração de processo e payload remoto têm riscos diferentes.

O que a validação de JSON não garante?

Mesmo um guard completo responde apenas a uma pergunta: este valor tem o formato que o código espera? Ele não prova que:

  • o usuário está autenticado ou autorizado;
  • o registro ainda existe ou está atualizado;
  • uma URL, data, identificador ou permissão faz sentido para o negócio;
  • o fornecedor cumpriu uma regra que não está representada no tipo;
  • o payload pode ser registrado sem expor dados sensíveis.

Essas decisões pertencem a camadas adicionais. A própria orientação da OWASP trata validação de entrada como uma defesa que precisa coexistir com outros controles. O guard impede que uma forma inesperada avance silenciosamente; ele não transforma uma fonte externa em fonte de verdade.

Perguntas frequentes

Posso usar as User depois de JSON.parse?

Pode, se você já tiver verificado o valor por outro caminho. Sozinha, a asserção só altera a visão do compilador. Para entrada externa, prefira unknown até uma função de validação retornar o tipo.

Preciso validar todos os campos da resposta?

Valide todos os campos que o código usará como se fossem confiáveis. Se o contrato interno exige apenas uma parte da resposta, extraia essa parte no guard e deixe o tipo refletir o que foi realmente estabelecido.

response.ok já prova que a resposta é segura?

Não. Ele ajuda a separar uma resposta HTTP de sucesso de uma resposta com erro. Ainda é necessário analisar o corpo e validar o formato antes de acessar propriedades específicas.

Conclusão

A sequência confiável é status HTTP -> texto -> JSON -> unknown -> guard -> valor de domínio. Ela torna visível o que falhou e evita que uma anotação otimista esconda dados inesperados.

Se o contrato crescer, substitua o guard por uma solução de schema que a equipe consiga manter. Não remova a fronteira. O objetivo é que a regra de negócio nunca precise descobrir, tarde demais, que o valor recebido não era o que o tipo prometia.

Fontes consultadas