Conceptos básicos de Zod
Define esquemas, valida datos en tiempo de ejecución y maneja errores - los fundamentos de la validación type-safe en TypeScript.
Busca en todas las páginas de la documentación
Define esquemas, valida datos en tiempo de ejecución y maneja errores - los fundamentos de la validación type-safe en TypeScript.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
import { z } from "zod";
// 1. Define un esquema
const UserSchema = z.object({
name: z.string().min(1, "El nombre es requerido"),
email: z.string().email("Email inválido"),
age: z.number().int().min(18, "Debe ser mayor de 18"),
});
// 2. Parse (lanza en caso de fallo)
try {
const user = UserSchema.parse({ name: "Ada", email: "ada@example.com", age: 30 });
console.log(user); // tipado como { name: string; email: string; age: number }
} catch (err) {
if (err instanceof z.ZodError) {
console.error(err.issues);
}
}
// 3. safeParse (nunca lanza)
const result = UserSchema.safeParse({ name: "", email: "bad", age: 15 });
if (!result.success) {
console.error(result.error.flatten());
} else {
console.log(result.data);
}Cuándo usarlo: Siempre que necesites validación en tiempo de ejecución de entrada de usuario, respuestas API, variables de entorno o cualquier límite de datos no confiables.
"use client";
import { useState } from "react";
import { z } from "zod";
const ContactSchema = z.object({
name: z.string().min(1, "El nombre es requerido").max(100, "El nombre es muy largo"),
email: z.string().email("Por favor ingresa un email válido"),
message: z
.string()
.min(10, "El mensaje debe tener al menos 10 caracteres")
.max(1000, "El mensaje es muy largo"),
});
type ContactData = z.infer<typeof ContactSchema>;
export function ContactValidator() {
const [errors, setErrors] = useState<Record<string, string[]>>({});
const [success, setSuccess] = useState<ContactData | null>(null);
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const raw = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};
const result = ContactSchema.safeParse(raw);
if (!result.success) {
const flat = result.error.flatten();
setErrors(flat.fieldErrors as Record<string, string[]>);
setSuccess(null);
} else {
setErrors({});
setSuccess(result.data);
}
}
return (
<form onSubmit={handleSubmit} className="flex max-w-md flex-col gap-3">
<div>
<input name="name" placeholder="Nombre" className="w-full rounded border p-2" />
{errors.name && <p className="text-sm text-red-600">{errors.name[0]}</p>}
</div>
<div>
<input name="email" placeholder="Email" className="w-full rounded border p-2" />
{errors.email && <p className="text-sm text-red-600">{errors.email[0]}</p>}
</div>
<div>
<textarea name="message" placeholder="Mensaje" className="w-full rounded border p-2" />
{errors.message && <p className="text-sm text-red-600">{errors.message[0]}</p>}
</div>
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">
Validar
</button>
{success && (
<pre className="rounded bg-green-50 p-3 text-sm">{JSON.stringify(success, null, 2)}</pre>
)}
</form>
);
}Lo que esto demuestra:
safeParse para validación sin lanzar excepcionesflatten() para obtener mensajes de error a nivel de campoz.infer para derivar tipos TypeScript desde esquemasparse() devuelve los datos validados o lanza un ZodError que contiene un array issuessafeParse() devuelve una unión discriminada: { success: true, data } o { success: false, error }ZodError.flatten() agrupa errores en formErrors (raíz) y fieldErrors (arrays por campo)ZodError.format() devuelve un objeto anidado que coincide con la forma del esquema, útil para objetos profundamente anidados.passthrough() o .strict() para cambiar)Mapas de error personalizados:
const schema = z.string({
required_error: "Este campo es requerido",
invalid_type_error: "Se esperaba un string",
}).min(1, { message: "No puede estar vacío" });Mapa de error global:
z.setErrorMap((issue, ctx) => {
if (issue.code === z.ZodIssueCode.too_small) {
return { message: `La longitud mínima es ${issue.minimum}` };
}
return { message: ctx.defaultError };
});Errores aplanados vs formateados:
const result = schema.safeParse(data);
if (!result.success) {
// Aplanado - excelente para formularios simples
result.error.flatten();
// { formErrors: string[], fieldErrors: { name?: string[], email?: string[] } }
// Formateado - excelente para objetos anidados
result.error.format();
// { name: { _errors: string[] }, address: { city: { _errors: string[] } } }
}// El tipo inferido coincide exactamente con el esquema
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number }
// ZodError es genérico - puedes tiparlo
const result = UserSchema.safeParse(data);
if (!result.success) {
const err: z.ZodError<User> = result.error;
}
// Usa z.ZodType para aceptar cualquier esquema como parámetro
function validate<T>(schema: z.ZodType<T>, data: unknown): T {
return schema.parse(data);
}parse vs safeParse en server actions - Usar parse dentro de un server action lanzará un error no manejado. Solución: Siempre usa safeParse en server actions y devuelve errores estructurados al cliente.
Coerción de strings desde FormData - FormData.get() devuelve string | File | null, pero tu esquema espera number. Solución: Usa z.coerce.number() o z.string().pipe(z.coerce.number()) cuando análices datos de formulario.
Eliminación silenciosa de claves desconocidas - z.object() elimina claves extra por defecto. Si las necesitas, usa .passthrough(). Si quieres rechazarlas, usa .strict().
Localidad del mensaje de error - Los mensajes de error de Zod son en inglés por defecto. Solución: Usa un errorMap personalizado para i18n.
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
| Yup | Quieres una librería de esquema nativa de Formik con una API similar | Necesitas inferencia de tipos TypeScript de primera categoría |
| Valibot | Necesitas el tamaño de bundle más pequeño posible | Depende de las integraciones del ecosistema extenso de Zod |
| ArkType | Quieres validación a nivel de tipo con overhead de tiempo de ejecución cercano a cero | Necesitas soporte y ejemplos de comunidad generalizados |
| Validación manual | Verificaciones puntuales con lógica trivial | Tienes más de 2-3 campos u objetos anidados |
De una aplicación SaaS Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo de producción: esquema Banner con defaults, nullable y validación entre campos
// Archivo: src/schemas/banner.ts
import { z } from 'zod';
export const BannerSchema = z.object({
title: z.string().min(1, 'El título es requerido').max(100),
subtitle: z.string().optional().default(''),
imageUrl: z.string().url('Debe ser una URL válida').nullable(),
linkUrl: z.string().url('Debe ser una URL válida').optional(),
linkText: z.string().optional().default('Más información'),
isActive: z.boolean().default(true),
startDate: z.coerce.date(),
endDate: z.coerce.date().nullable(),
priority: z.number().int().min(0).max(100).default(50),
}).refine(
(data) => {
if (data.endDate && data.startDate) {
return data.endDate > data.startDate;
}
return true;
},
{
message: 'La fecha de fin debe ser después de la fecha de inicio',
path: ['endDate'],
}
);
// Extrae el tipo TypeScript del esquema
export type Banner = z.infer<typeof BannerSchema>;
// {
// title: string;
// subtitle: string; // defaults a ''
// imageUrl: string | null; // nullable
// linkUrl?: string; // optional, sin default
// linkText: string; // defaults a 'Más información'
// isActive: boolean; // defaults a true
// startDate: Date;
// endDate: Date | null;
// priority: number; // defaults a 50
// }Lo que esto demuestra en producción:
.optional() significa que el campo puede ser undefined (omitido de la entrada). .nullable() significa que puede ser explícitamente null. Estos son diferentes: imageUrl debe estar presente pero puede ser null, mientras que linkUrl puede omitirse completamente..optional().default('') hace que un campo sea opcional en la entrada pero garantiza un valor en la salida. Después de analizar, subtitle es siempre un string, nunca undefined..refine() permite validación entre campos que z.string() o z.date() solo no pueden expresar. La opción path: ['endDate'] adjunta el mensaje de error al campo correcto en las vistas de errores del formulario.z.coerce.date() convierte entradas de string (como "2025-01-15" desde una entrada de fecha HTML o payload JSON) en objetos Date automáticamente. Sin coerción, pasar un string a un esquema z.date() fallaría.z.infer<typeof BannerSchema> extrae el tipo TypeScript del esquema. Esta es la única fuente de verdad. Nunca escribes manualmente una interfaz coincidente, lo cual elimina desviación entre validación y tipos.parse() devuelve los datos validados o lanza un ZodErrorsafeParse() nunca lanza; devuelve { success: true, data } o { success: false, error }safeParse en server actions y en cualquier lugar donde necesites manejar errores elegantementeconst result = schema.safeParse(data);
if (!result.success) {
const flat = result.error.flatten();
// flat.fieldErrors = { name?: string[], email?: string[] }
}flatten() devuelve { formErrors: string[], fieldErrors: { [key]: string[] } } -- mejor para formularios simplesformat() devuelve un objeto anidado que coincide con la forma del esquema con arrays _errors -- mejor para objetos profundamente anidadosconst UserSchema = z.object({
name: z.string(),
age: z.number(),
});
type User = z.infer<typeof UserSchema>;
// { name: string; age: number }Las elimina silenciosamente. Usa .passthrough() para mantener claves extra o .strict() para rechazarlas con un error.
Usa z.coerce.number() que llama a Number(value) antes de validar, convirtiendo el string a número automáticamente.
.optional() permite undefined (el tipo se convierte en T | undefined).nullable() permite null (el tipo se convierte en T | null).nullish() permite ambos (el tipo se convierte en T | null | undefined)z.setErrorMap((issue, ctx) => {
if (issue.code === z.ZodIssueCode.too_small) {
return { message: `La longitud mínima es ${issue.minimum}` };
}
return { message: ctx.defaultError };
});parse() lanza un ZodError en caso de fallo, que se convierte en un error de servidor no manejado. Siempre usa safeParse() en server actions y devuelve errores estructurados al cliente.
z.coerce.date() llama a new Date(value), así que acepta timestamps y números aleatorios. Si solo quieres strings de fecha ISO, usa z.string().datetime() en su lugar.
Agrega validación entre campos. Proporcionas un predicado que recibe el objeto completamente analizado y devuelve true/false. Especifica path: ["fieldName"] para adjuntar el error a un campo específico.
function validate<T>(schema: z.ZodType<T>, data: unknown): T {
return schema.parse(data);
}Sí. z.ZodError<User> te da un error tipado cuyos issues hacen referencia a los campos de User. Esto es útil para utilidades de manejo de errores type-safe.
Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥