Skip to content
This page has been auto-translated and may contain errors.View in English

Fundamentos de Zod

Van tres errores hasta ahora, todos con la misma forma. Llegó un valor, el código lo usó y nadie lo verificó antes.

Corregir cada uno donde aparece funciona, pero es interminable. Cada nuevo destino es un lugar más que hay que recordar.

La otra opción es verificar el valor una sola vez, apenas llega, contra una descripción de lo que esperabas. Esa descripción es un schema, y Zod es la librería que esta sección usa para escribir uno.

Qué es un schema

Imagina el escáner de un aeropuerto. Tu maleta entra por un lado. La máquina está configurada con reglas: nada de objetos filosos, nada de líquidos que superen cierto tamaño. Una maleta que cumple las reglas sale por el otro lado sin cambios. Una que no las cumple jamás pasa.

Un schema son los ajustes de la máquina. Parsear es hacer pasar la maleta por ella.

Instálalo e impórtalo:

bash
npm install zod
js
import * as z from 'zod'

El schema más simple es una sola regla sobre un solo valor:

js
const teacherSchema = z.string()

const teacher = 'Mateo'

console.log(teacherSchema.parse(teacher))
// 'Mateo'

parse le entrega los datos a la máquina. Los datos cumplen la regla, así que vuelven intactos.

Dale algo que no encaje y la máquina se detiene:

js
teacherSchema.parse(12345)
// ZodError: Invalid input: expected string, received number

Ese es todo el modelo. Describes lo que es aceptable, haces pasar los valores, y obtienes el valor de vuelta o un error.

JunoQué es un schema Vale la pena separar dos palabras desde el principio, porque se suelen usar como si fueran lo mismo y aquí significan cosas distintas.

El schema es la descripción de lo que vas a aceptar. Parsear es el acto de verificar algo contra esa descripción. Escribes el schema una vez y lo usas para parsear tantas veces como quieras.

JunoQué es un schema Ten en cuenta que parse devuelve el valor, no un verdadero o falso. Eso es intencional: es un punto de control por el que haces pasar los datos, no una prueba que corres al lado.

El hábito que se deriva de esto es dejar de usar el dato crudo después de parsearlo y usar en su lugar el valor devuelto. Hoy son los mismos datos, pero esto significa que el valor parseado es el que sigue fluyendo cuando empieces a agregar valores por defecto y coerción.

JunoQué es un schema La API es inmutable, algo tan sutil que se puede pasar por alto y provocar un error real. Cada método devuelve un nuevo schema en lugar de modificar aquel sobre el que se llamó, así que schema.min(3) es un valor que tienes que guardar.

Si lo llamas y descartas el resultado, el original queda igual, sin la restricción, y sin ningún aviso de que falta.

parse lanza una excepción, lo cual funciona bien en un límite donde una falla debería detener la solicitud, pero no sirve de nada cuando quieres inspeccionar esa falla. La alternativa que devuelve un objeto de resultado en lugar de lanzar es la que vas a querer en un formulario o en un route handler, y se cubre en el próximo capítulo.

Describir un objeto

Un profesor rara vez es un solo string. Mejor dale una forma al schema:

js
const teacherSchema = z.object({
  name: z.string(),
  age: z.number(),
})

const teacher = {
  name: 'Mateo',
  age: 21,
}

console.log(teacherSchema.parse(teacher))
// { name: 'Mateo', age: 21 }

Cada entrada dentro de z.object() es una clave, y su valor es el schema de esa clave. Ahora la máquina espera un objeto con un name que sea cualquier string y una age que sea cualquier número.

Rompe una de ellas y el error dice exactamente cuál:

js
teacherSchema.parse({ name: 'Mateo', age: '21' })
// ZodError: Invalid input: expected number, received string

Zod trae los primitivos que uno esperaría, z.string(), z.number(), z.boolean(), y los objetos se anidan dentro de otros objetos tan profundo como lo necesiten tus datos.

JunoDescribir un objeto El anidamiento es lo que hace que esto escale. Un schema para un objeto se construye a partir de los schemas de sus campos, y cada uno de esos campos puede a su vez ser un objeto.

Así, la misma idea cubre tanto un formulario de dos campos como la respuesta de una API con una estructura profunda. Siempre estás describiendo un nivel a la vez.

JunoDescribir un objeto Conviene saber esto antes de que te tome por sorpresa: z.object() ignora las claves que no describiste. Si parseas { name: 'A', age: 1, extra: 'x' } contra un schema que solo describe name y age, obtienes de vuelta { name: 'A', age: 1 }, sin ningún error y sin el campo extra.

Eso suele ser lo que quieres en un límite del sistema, porque significa que un atacante no puede colar un campo extra en lo que hagas con el objeto parseado. Pero también significa que un error de tipeo en el nombre de un campo falla en silencio, así que un valor que esperabas que estuviera ahí queda ausente sin ningún aviso.

JunoDescribir un objeto Cuando quieras un comportamiento más estricto, z.strictObject() lanza un error ante claves no reconocidas en lugar de descartarlas. Verificado en Zod 4.5.4: z.object() las elimina, z.strictObject() lanza una excepción.

Cuál elegir es una decisión real, no una cuestión de estilo. Eliminar las claves extra es la opción por defecto más segura para un endpoint público, porque los clientes agregan campos y no quieres romperles nada.

El modo estricto le queda bien a las llamadas internas entre servicios, donde una clave inesperada suele significar que ambos lados se desincronizaron. Es mejor enterarte de eso de inmediato que depurar un campo descartado en silencio una semana después.

El único lugar donde eliminar claves te puede morder es cuando piensas en términos de asignación masiva. Esto protege al objeto parseado, pero no hace nada por el código que se salta el parseo y lee req.body directamente. Parsea una vez y después no vuelvas a mirar el dato crudo.

Por qué la verificación tiene que pasar en tiempo de ejecución

TypeScript también describe formas:

ts
type Teacher = {
  name: string
  age: number
}

Esto se parece al schema, pero hace un trabajo completamente distinto. TypeScript verifica los tipos mientras escribes el código. Cuando compila a JavaScript, cada anotación desaparece, así que nada de eso sobrevive en el programa en ejecución.

Eso está bien para los valores que produce tu propio código, y no sirve de nada para los que no. El cuerpo de una solicitud, la respuesta de una API, el envío de un formulario: todo eso llega mientras el programa ya está corriendo, mucho después de que los tipos dejaron de existir. TypeScript asume que tus datos son correctos. Zod los verifica.

Un schema es la descripción de un tipo que todavía existe cuando los datos llegan.

JunoPor qué la verificación tiene que pasar en tiempo de ejecución Esto confunde a mucha gente porque parece que ambas cosas verifican lo mismo, y solo una de ellas sigue presente cuando realmente importa.

TypeScript es una conversación contigo mientras escribes. El schema es una conversación con los datos mientras el programa se ejecuta.

JunoPor qué la verificación tiene que pasar en tiempo de ejecución Tipar el cuerpo de una solicitud es donde esto se nota más seguido. Convertirlo a un tipo que tú escribiste, con req.body as SignupFields, compila sin problemas y no verifica nada, porque una aserción de tipo es simplemente decirle al compilador que deje de preguntar.

Cada campo que leas después es una suposición. Si en cambio parseas el cuerpo con un schema, esa suposición se convierte en un hecho.

JunoPor qué la verificación tiene que pasar en tiempo de ejecución La línea que vale la pena trazar es esta: todo lo que cruzó un límite de proceso no tiene tipo, sin importar lo que digan tus anotaciones. El cuerpo de las solicitudes, los query strings, las variables de entorno, el JSON leído desde disco, las respuestas de un servicio que le pertenece a tu propio equipo, las filas de una base de datos cuya migración todavía no corriste.

Las respuestas de APIs de terceros son las que la gente suele dejar afuera, con el argumento de que el proveedor documenta la forma de los datos. Pero los proveedores lanzan cambios, devuelven objetos parciales durante incidentes, y agregan nulls en campos que nunca antes habían sido null. Un schema en ese límite convierte una falla confusa en las profundidades de tu código en una falla clara justo en el punto de entrada.

Por esto Zod vale lo que pesa: no tiene dependencias, y funciona igual en Node y en el navegador, así que un mismo schema puede servir tanto a un route handler como al formulario que le envía datos.

Ponlo en práctica

Escríbelo desde cero, sin copiar el ejemplo del profesor.

  1. Importa Zod.
  2. Crea un characterSchema con dos claves: name, un string, y episode, un número.
  3. Define un objeto character con el nombre 'Luke Skywalker' y episodio 4.
  4. Valida el personaje contra el schema y muestra el resultado en consola.
  5. Cambia episode al string '4' y predice qué va a pasar antes de ejecutarlo.
Compara tus respuestas
js
import * as z from 'zod'

const characterSchema = z.object({
  name: z.string(),
  episode: z.number(),
})

const character = {
  name: 'Luke Skywalker',
  episode: 4,
}

console.log(characterSchema.parse(character))
// { name: 'Luke Skywalker', episode: 4 }

Dos claves significan z.object() en lugar de un primitivo simple, y cada clave tiene su propio schema.

Con episode como '4', parse lanza ZodError: Invalid input: expected number, received string. El string '4' no es un número, y Zod no lo va a convertir por ti en silencio. Convertir a propósito es una instrucción aparte, que se cubre en el próximo capítulo.

El episodio 4 es correcto, dicho sea de paso. Luke Skywalker aparece por primera vez en la película original de Star Wars de 1977, que después fue numerada como Episodio IV.

Hacia dónde va esto

Por ahora un schema hace una sola cosa: acepta un valor o lanza una excepción. Eso alcanza para describir datos, pero todavía no alcanza para construir con ellos.

Inferir tipos y forzar la conversión de datos de entrada agrega las dos piezas que lo hacen práctico. Un mismo schema también le puede dar el tipo a TypeScript, así que la forma se escribe una sola vez. Y un parseo fallido puede darte un resultado para inspeccionar en lugar de una excepción para atrapar.