JSON.parse terminó, pero el valor devuelto todavía no es un User. Es solo un valor de JavaScript que llegó desde fuera del proceso. Una interfaz de TypeScript describe lo que el código espera recibir; no inspecciona una respuesta HTTP, un archivo o un mensaje de una cola.

Un límite seguro es pequeño y explícito: comprobar el estado de la respuesta, analizar el texto, validar la forma y solo entonces entregar un valor tipado a la lógica de negocio. El patrón siguiente no usa librerías. Después puedes sustituir el guard por Zod, Valibot, JSON Schema o un cliente generado sin cambiar la idea central.

El mismo cuidado se aplica cuando validas argumentos antes de ejecutar una tool. Cambia el origen, pero el contrato es el mismo: los datos externos deben comprobarse antes de producir efectos.

Flujo desde el JSON externo hasta un valor tipado

En esta guía construirás un guard para un objeto anidado, lo conectarás con fetch, probarás entradas rotas y separarás lo que la validación de la forma realmente demuestra.

¿Puede TypeScript validar JSON en runtime?

No. El compilador puede ayudar a estrechar un valor después de que se ejecute una condición, pero no observa el contenido recibido por la red. TypeScript for the New Programmer explica que los tipos se eliminan al transformar el código, y la referencia de Basic Types aclara que una aserción de tipo no hace ninguna comprobación en runtime.

Este código compila, pero no valida nada:

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

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

Si body contiene { "id": 42 }, la aserción no corrige el número ni detecta que falta name. Solo le dice al compilador que acepte tu promesa. Empieza con unknown para la entrada externa y estrecha el valor en runtime, como explica el Handbook de Narrowing de TypeScript.

La decisión importante no es elegir entre un guard escrito a mano y una librería. Es definir el contrato en el límite y hacer que el resto del programa reciba solo el resultado que pasó ese contrato. La herramienta puede cambiar; el límite sigue siendo responsabilidad del código.

¿Cuál es la diferencia entre analizar JSON y validar su forma?

Son fallos distintos. Analizar convierte texto JSON válido en un valor de JavaScript. Validar la forma comprueba si ese valor tiene los campos y tipos que el programa necesita. JSON.parse lanza un SyntaxError para texto inválido, según la referencia de MDN sobre JSON.parse, pero un JSON sintácticamente válido puede tener cualquier forma.

Haz que el parseo devuelva unknown y mantén esta etapa sin afirmar nada sobre el dominio:

function parseJson(text: string): unknown {
  try {
    return JSON.parse(text) as unknown;
  } catch (error) {
    if (error instanceof SyntaxError) {
      throw new Error("La respuesta no contiene JSON válido", { cause: error });
    }

    throw error;
  }
}

El as unknown no añade protección. Hace visible la intención: el parseo terminó y otra función todavía debe comprobar el valor. Esta separación también hace que las pruebas sean más precisas, porque una entrada puede fallar por sintaxis o por forma.

¿Cómo escribir un guard para un objeto anidado?

Un guard es una función que devuelve un predicado de tipo, como value is User, después de comprobar propiedades en runtime. TypeScript documenta los predicados de tipo y el narrowing como la forma de indicar al compilador qué estableció realmente una condición.

El ejemplo siguiente valida un usuario, incluido un campo anidado y una lista. Rechaza los valores que no cumplen el contrato y devuelve User solo en el camino aprobado:

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("User debe ser un 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("La forma de User no es válida");
  }

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

El valor de retorno toma una decisión deliberada: crea un objeto nuevo con los campos conocidos. Los campos adicionales del payload no pasan a formar parte del contrato interno por accidente. Si la aplicación necesita conservarlos, esa decisión debe aparecer en el tipo y en las pruebas.

Este artículo no presenta un caso de cliente ni un benchmark propio. El valor del ejemplo es que es pequeño, ejecutable y fácil de adaptar al contrato real de una aplicación.

¿Cómo conectar el guard con una respuesta HTTP?

Una respuesta HTTP tiene al menos dos dimensiones relevantes: el resultado del transporte y el contenido. fetch de Node.js expone ok, status y métodos para leer el cuerpo mediante Response y sus objetos globales. Un estado exitoso no demuestra que el JSON tenga la forma esperada.

Une las etapas sin convertir un fallo en otro:

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

  if (!response.ok) {
    throw new Error(`El servicio de usuarios respondió ${response.status}`);
  }

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

Ahora la llamada falla en puntos observables: el servicio puede devolver un error HTTP, el cuerpo puede no ser JSON o el JSON puede incumplir el contrato. Para endpoints que reciben llamadas de terceros, también puedes probar el contrato real de una entrega HTTP, incluidas firmas, duplicados y reintentos.

No escondas el estado dentro del guard. readUser debe conocer los datos; fetchUser debe conocer el transporte. Esta división mantiene reutilizable la función de forma para archivos, colas y respuestas ya leídas.

¿Cómo probar el límite sin confiar en el camino feliz?

Prueba el contrato donde llega la entrada. La guía de OWASP sobre validación de entradas recomienda una validación explícita y restrictiva cuando los datos entran en un sistema, pero esta etapa no sustituye la autenticación, la autorización ni los controles específicos de la operación.

Con cualquier runner de pruebas, la idea mínima es cubrir sintaxis, forma, anidamiento, arrays y éxito:

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("El payload válido debería pasar");
}

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

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

  if (!rejected) {
    throw new Error("La entrada inválida pasó");
  }
}

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);
}

En un proyecto real, añade casos para campos opcionales, null, arrays vacíos, valores extremos y cambios de versión del proveedor. Prueba también el cuerpo de un error HTTP cuando la capa de transporte forme parte del contrato.

Un mensaje de error útil conserva el punto donde terminó la confianza. "JSON inválido", "estado 404" y "falta el campo active" ayudan más a diagnosticar que convertir todos los caminos en "no se pudo cargar el usuario".

¿Cuándo conviene sustituir el guard por una librería de schemas?

Un guard escrito a mano puede bastar para un contrato pequeño y estable. Una librería de schemas resulta útil cuando varios endpoints comparten reglas, los mensajes de error necesitan estructura o el schema también alimenta la documentación y la generación de tipos. JSON Schema y los clientes generados pueden encajar mejor cuando varios equipos mantienen un contrato formal.

El criterio no es eliminar código manual a cualquier precio. Es mantener una única fuente de verdad y asegurar que la validación ocurre en el límite correcto. En una aplicación con tools, la misma decisión aparece al validar argumentos y salidas de una tool. Para la configuración local, consulta cómo separar los tipos de validación en runtime, porque la configuración del proceso y los payloads remotos tienen riesgos distintos.

¿Qué no garantiza la validación de JSON?

Incluso un guard completo responde solo a una pregunta: ¿este valor tiene la forma que espera el código? No demuestra que:

  • el usuario esté autenticado o autorizado;
  • el registro siga existiendo o esté actualizado;
  • una URL, fecha, identificador o permiso tenga sentido para el negocio;
  • el proveedor haya cumplido una regla que no está representada en el tipo;
  • el payload pueda registrarse sin exponer datos sensibles.

Esas decisiones pertenecen a capas adicionales. La orientación de OWASP trata la validación de entradas como una defensa que debe coexistir con otros controles. El guard evita que una forma inesperada avance en silencio; no convierte una fuente externa en una fuente de verdad.

Preguntas frecuentes

¿Puedo usar as User después de JSON.parse?

Puedes hacerlo si ya comprobaste el valor por otra vía. Por sí sola, la aserción solo cambia la visión del compilador. Para una entrada externa, prefiere unknown hasta que una función de validación devuelva el tipo.

¿Tengo que validar todos los campos de la respuesta?

Valida cada campo que el código vaya a tratar como confiable. Si el contrato interno necesita solo una parte de la respuesta, extrae esa parte en el guard y haz que el tipo refleje lo que realmente se estableció.

¿response.ok demuestra que la respuesta es segura?

No. Ayuda a separar una respuesta HTTP exitosa de una respuesta con error. Aún debes leer el cuerpo y validar su forma antes de acceder a propiedades concretas.

Conclusión

La secuencia confiable es estado HTTP -> texto -> JSON -> unknown -> guard -> valor de dominio. Hace visibles los fallos y evita que una aserción optimista oculte datos inesperados.

Si el contrato crece, sustituye el guard por una solución de schemas que el equipo pueda mantener. No elimines el límite. El objetivo es que la lógica de negocio no descubra demasiado tarde que el valor recibido no era lo que prometía el tipo.

Fuentes consultadas