Busca en todas las páginas de la documentación
Da forma, realiza coerción y añade lógica de validación personalizada con transform, refine, superRefine, preprocess y pipe.
Tarjeta de referencia rápida — lista para copiar y pegar.
import { z } from "zod";
// transform — cambia el valor de salida
const Trimmed = z.string().transform((s) => s.trim());
const Lower = z.string().transform((s) => s.toLowerCase());
// refine — predicado de validación personalizada
const EvenNumber = z.number().refine((n) => n % 2 === 0, {
message: "Debe ser un número par",
});
// superRefine — múltiples problemas, control total
const PasswordSchema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Al menos 8 caracteres" });
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Al menos una letra mayúscula" });
}
if (!/\d/.test(val)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Al menos un dígito" });
}
});
// preprocess — realiza coerción antes de la validación
const CoercedNumber = z.preprocess((val) => Number(val), z.number().positive());
// pipe — encadena esquemas juntos
const StringToNumber = z.string().pipe(z.coerce.number().int().positive());Cuándo usarlo: Cuando la validación de tipo básica no es suficiente — necesitas normalizar datos, aplicar reglas de negocio o encadenar transformaciones.
"use client";
import { useState } from "react";
import { z } from "zod";
const SignupSchema = z
.object({
username: z
.string()
.min(3)
Lo que esto demuestra:
transform para normalizar el nombre de usuario y el correo electrónicosuperRefine para validación de contraseña con múltiples reglaspreprocess para convertir la edad de string a número.refine a nivel de objeto para validación entre campos (coincidencia de contraseña)false crea un único problemaRefinementCtx para que puedas añadir múltiples problemasz.input y z.output pueden diferirRefine asincronismo para verificaciones del lado del servidor:
const UniqueEmail = z.string().email().refine(
async (email) => {
const exists = await db.user.findUnique({ where: { email } });
return !exists;
},
{ message: "Correo electrónico ya registrado" }
Encadenamiento de transformaciones:
const CsvToNumbers = z
.string()
.transform((s) => s.split(","))
.transform((arr) => arr.map(Number))
.pipe(z.array(z.Tipos marcados con refine:
const UserId = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserId>; // string & { __brand: "UserId" }
function getUser(id: UserId) { /* ... */ }
Canalización de default + transform:
const Config = z.object({
port: z.coerce.number().default(3000),
host: z.string().default("localhost").transform((h) => h.toLowerCase()),
debug: z.preprocess((
// Cuando un esquema tiene transformaciones, los tipos de entrada y salida difieren
const S = z.string().transform((s) => s.length);
type SInput = z.input<typeof S>; // string
type SOutput = z.
refine solo se ejecuta si la validación base pasa — Si z.string().email().refine(...) recibe un no-email, el callback refine nunca se llama. Esto es generalmente deseable pero puede sorprenderte si esperas todos los errores a la vez.
transform cambia el tipo — Después de .transform(), los validadores anteriores ven el tipo transformado. Si encadenas .min() después de .transform(), valida el valor transformado. Solución: Coloca los validadores antes de las transformaciones.
preprocess vs coerce — z.preprocess es un gancho de propósito general; z.coerce.* es un atajo para z.preprocess(Constructor, ...). Prefiere z.coerce para la conversión de tipo simple.
Los refinamientos asincronos requieren parseAsync — Llamar a .parse() en un esquema con refinamientos asincronos lanza un error. Solución: Siempre usa parseAsync o safeParseAsync.
Ruta refine a nivel de objeto — Si omites path en un .refine() a nivel de objeto, el error aparece en formErrors en lugar de fieldErrors. Solución: Siempre especifica path: ["fieldName"] para refinamientos a nivel de objeto.
| Alternativa | Usa Cuando | No Uses Cuando |
|---|---|---|
Yup .test() | Usas Formik y necesitas validadores personalizados | Quieres tipos transformados inferidos de TypeScript |
Valibot transform | Necesitas transformaciones tree-shakeable con paquete mínimo | Confías en .brand() o .pipe() específicos de Zod |
| Funciones de validación personalizada | La lógica es trivial (un campo, una comprobación) | Tienes dependencias complejas entre múltiples campos |
| Decoradores Class-validator | Prefieres validación basada en decoradores en modelos de clase | Trabajas en una base de código funcional / basada en componentes |
transform cambia el valor de salida (y posiblemente su tipo)refine añade un predicado de validación personalizada única (devuelve true/false)superRefine proporciona acceso completo a RefinementCtx para que puedas añadir múltiples problemasz.coerce.* es un atajo para conversión de tipo simple (p. ej., string a número a través de Number()). Usa z.preprocess cuando necesites lógica de coerción personalizada más allá de lo que proporciona un constructor nativo.
pipe pasa la salida de un esquema como entrada a otro. Es útil para transformaciones de múltiples pasos:
const CsvToNumbers = z.string()
.transform((s) => s.split(","))
.transform((arr) => arr.map(Number))
.pipe(z.const Email = z.string().email().transform((s) => s.toLowerCase().trim());
const Username = z.string().min(3).transform((sconst Password = z.string().superRefine((val, ctx) => {
if (val.length < 8)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Al menos 8 caracteres" });
if (!/
Usa .refine() en el esquema de objeto con una opción path:
schema.refine((d) => d.password === d.confirmPassword, {
message: "Las contraseñas no coinciden",
path: ["confirmPassword"],
});const UniqueEmail = z.string().email().refine(
async (email) => !(await db.user.findUnique({ where: { email } })),
{ message: "Correo electrónico ya registrado" }
);
// Debe usar parseAsync o safeParseAsyncNo. Si z.string().email().refine(...) recibe un no-email, el callback refine nunca se llama. La validación base debe pasar primero. Esto significa que no verás todos los errores a la vez si el tipo base es incorrecto.
Lanza un error. Debes usar parseAsync() o safeParseAsync() para cualquier esquema que contenga refinamientos asincronos.
Después de .transform(), los validadores anteriores ven el valor transformado. Si encadenas .min() después de .transform(), valida la salida transformada, no la entrada original.
const UserId = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserId>;
// string & { __brand: "UserId" }
// Evita pasar strings arbitrarios donde se espera un UserIdconst S = z.string().transform((s) => s.length);
type SInput = z.input<typeof S>; // string
type SOutput = z.outputSí. Usa una devolución de predicado de tipo:
const NonEmpty = z.array(z.string()).superRefine(
(arr, ctx): arr is [string, ...string[]] => {
if (arr.length ===
Coloca los validadores antes de las transformaciones para que verifiquen la entrada sin procesar.
Los tipos de entrada y salida divergen cuando una transformación cambia el tipo.