Zod en 2026: validación de datos con TypeScript en el runtime que no falla

TypeScript te da seguridad de tipos mientras escribes código. El problema llega cuando ese código se ejecuta: el compilador ya no está, los tipos desaparecen y lo que tienes es JavaScript puro corriendo contra datos del mundo real. Si recibes el body de una petición POST, la respuesta de una API externa o los parámetros de una URL, TypeScript no puede hacer nada por ti en ese momento. Tú decides si confías en esos datos o los verificas.

Ahí es donde entra Zod. No sustituye a TypeScript, lo complementa: define esquemas que validan los datos en runtime y de paso deriva el tipo TypeScript automáticamente. Un solo lugar de verdad para la forma que deben tener tus datos.

El problema real: los tipos son mentiras en producción

Cuando escribes const user = data as User en TypeScript, el compilador te cree. Pero esa aserción es solo una promesa que tú haces: estás diciéndole al compilador "confía en mí, esto es un User". Si la API te devuelve un campo con tipo incorrecto, un campo nulo inesperado o directamente un objeto distinto al que esperas, tu código compilará sin errores y petará en producción.

El error clásico: Cannot read properties of undefined (reading 'name'). Alguien asumió que data.user existía y tenía un campo name. TypeScript no pudo avisar porque nadie validó los datos cuando llegaron.

Con Zod el flujo cambia. Defines el esquema, pasas los datos por él y si no cumplen la forma esperada falla de manera controlada y explicativa, no de forma silenciosa a las 3 de la mañana.

Definir un esquema y derivar el tipo

La instalación es sencilla:

npm install zod

Y el uso básico:

import { z } from 'zod';

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof userSchema>;

El tipo User se deriva directamente del esquema. No tienes que definirlo a mano y mantenerlo sincronizado: si cambias el esquema, el tipo cambia también. Esto elimina una categoría entera de bugs donde el tipo TypeScript y la validación real no coincidían.

.parse() vs .safeParse()

Zod ofrece dos formas de validar datos. Con .parse() obtienes el valor validado si todo va bien, o una excepción si algo falla:

const user = userSchema.parse(data); // lanza ZodError si falla

Con .safeParse() nunca lanza excepciones: devuelve un objeto con success: true y los datos, o success: false y el error:

const result = userSchema.safeParse(data);

if (!result.success) {
  console.error(result.error.issues);
  return;
}

// Aquí TypeScript sabe que result.data es User
console.log(result.data.name);

En aplicaciones web con manejo de errores estructurado, .safeParse() casi siempre es la mejor opción. Con .parse() necesitas un try/catch; con .safeParse() el flujo de error queda explícito en el código sin bloques de excepción.

Transformaciones: validar y convertir a la vez

Zod no solo valida, también puede transformar los datos durante la validación. Esto es especialmente útil con datos que llegan en un formato y necesitas usarlos en otro.

// Elimina espacios en blanco al validar
const trimmedString = z.string().transform(s => s.trim());

// Query params siempre llegan como string, z.coerce los convierte
const pageSchema = z.coerce.number().min(1);

// Valida formato ISO 8601 y convierte a Date
const dateSchema = z.string().datetime().transform(s => new Date(s));

El tipo resultante refleja la transformación. Si usas z.string().transform(Number), Zod sabe que el output es numberstring. No tienes que añadir aserciones de tipo después.

Validaciones personalizadas

En Zod v4 existe el método .check() para añadir validaciones custom de forma clara:

const passwordSchema = z.string()
  .min(8)
  .check(s => s.match(/[A-Z]/) !== null, {
    message: 'Debe contener al menos una mayúscula',
  });

Tipos más complejos

Zod cubre bien los casos habituales que van más allá de objetos planos:

// Arrays de objetos validados
const usersSchema = z.array(userSchema);

// Unión de tipos
const idSchema = z.union([z.string(), z.number()]);

// Objeto con claves dinámicas
const scoresSchema = z.record(z.string(), z.number());

// Unión discriminada (más eficiente que z.union para objetos)
const eventSchema = z.discriminatedUnion('tipo', [
  z.object({ tipo: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ tipo: z.literal('keypress'), key: z.string() }),
]);

La unión discriminada merece atención especial. Cuando tienes varios objetos posibles con un campo que los distingue, z.discriminatedUnion es más rápido que z.union porque no tiene que probar todos los schemas: va directo al que corresponde según el valor discriminador.

Validación de APIs: el caso más frecuente

El sitio donde Zod rinde más es en los endpoints que reciben datos externos. Un ejemplo con Express:

const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).max(5).optional(),
});

app.post('/posts', (req, res) => {
  const result = createPostSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({
      error: 'Datos inválidos',
      issues: result.error.issues,
    });
  }

  // result.data está tipado correctamente
  const post = createPost(result.data);
  res.json(post);
});

Con tRPC o Hono la integración es más directa: ambos frameworks aceptan schemas de Zod nativamente para validar inputs y tipar automáticamente los endpoints. Defines el schema una vez y el framework se encarga de validar las peticiones entrantes y de tipar la función handler.

Prisma también usa Zod internamente para generar los tipos de los modelos, lo que facilita combinar validación de inputs con el schema de base de datos sin duplicar definiciones.

Qué cambia en Zod v4

Zod v4 salió en beta durante 2025 con dos mejoras concretas que importan en producción.

El bundle se reduce de forma notable. Zod v3 pesaba cerca de 57KB minificado, lo que en aplicaciones frontend se notaba. v4 baja ese número de manera significativa, especialmente si usas tree-shaking y solo importas lo que necesitas.

El parsing es más rápido, sobre todo con schemas grandes o anidados. En aplicaciones con mucho tráfico o validaciones pesadas, esto se traduce en menor latencia.

También llega z.config() para configuración global, útil para internacionalizar los mensajes de error:

import { z, ZodError } from 'zod';
import { es } from 'zod/locales/es';

z.config({ error: es });

Y el ya mencionado .check() como método unificado para validaciones custom, que en v3 se hacía con .refine() y .superRefine() con APIs distintas según el caso.

Alternativas que vale la pena conocer

El espacio de validación en TypeScript tiene más opciones ahora que hace dos años. Conocerlas ayuda a elegir bien según el proyecto.

Valibot: bundle muy pequeño, alrededor de 1KB para esquemas básicos gracias a su diseño modular. La API es parecida a Zod y el tree-shaking funciona mucho mejor porque cada validador es una función independiente. Buena opción si el tamaño del bundle es una restricción seria.

Arktype: define tipos desde strings de TypeScript con una sintaxis expresiva. Tiene inferencia de tipos muy potente y es rápido, aunque la curva de aprendizaje es mayor que Zod para equipos que no la conocen.

TypeBox: los schemas son JSON Schema válidos, lo que los hace reutilizables fuera de TypeScript. Si necesitas compartir la definición de tipos con servicios en otros lenguajes o con documentación OpenAPI, TypeBox tiene ventaja clara aquí.

Aun así Zod sigue siendo la primera opción por defecto en la mayoría de proyectos TypeScript. El ecosistema que se ha construido alrededor, con integraciones en tRPC, React Hook Form, Prisma, Fastify y decenas de librerías más, es difícil de igualar. Cuando el equipo ya conoce Zod y las dependencias lo soportan nativamente, cambiar a una alternativa raramente compensa.

Un patrón para llevarte

Si solo aplicas una cosa de este artículo, que sea esta: cada vez que recibas datos de fuera de tu aplicación, válidos con Zod antes de usarlos. Peticiones HTTP, respuestas de APIs externas, variables de entorno, datos de localStorage, parámetros de URL. Todo lo que viene de fuera.

// Variables de entorno tipadas y validadas al arrancar
const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(32),
  PORT: z.coerce.number().default(3000),
});

const env = envSchema.parse(process.env);
// Si falta una variable obligatoria, falla aquí en el arranque,
// no de forma silenciosa 10 minutos después en producción

Validar en runtime no reemplaza a TypeScript, pero sí cierra el hueco que TypeScript no puede cubrir. Los dos juntos dan una capa de seguridad que por separado ninguno alcanza.

Si quieres entender mejor qué pasa con los tipos en tiempo de compilación y por qué desaparecen, el artículo sobre TypeScript: validación en compile time vs runtime va al fondo de eso. Y si te interesa cómo JavaScript maneja los datos antes de que TypeScript entre en escena, el artículo sobre JavaScript y la validación de datos es buen punto de partida.

Imagen: Pexels / Tara Winstead

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP