Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Si ya usas Zod para validación básica, estos patrones avanzados te resuelven los casos reales que aparecen en producción: formularios que cambian según selecciones, datos que necesitan transformación, y schemas que se componen entre si.
Si necesitas la base primero, arranca con la guía de Zod para validación.
Discriminated Unions
El patron más útil para formularios dinámicos. Un campo "discriminador" determina que validaciones aplican:
import { z } from "zod";
const notificationSchema = z.discriminatedUnion("channel", [
z.object({
channel: z.literal("email"),
emailAddress: z.string().email(),
subject: z.string().min(1),
}),
z.object({
channel: z.literal("sms"),
phoneNumber: z.string().regex(/^\+\d{10,15}$/),
}),
z.object({
channel: z.literal("push"),
deviceToken: z.string().min(10),
}),
]);
type Notification = z.infer<typeof notificationSchema>;
// TypeScript sabe que si channel es "email", tiene emailAddress y subjectconst result = notificationSchema.safeParse({
channel: "email",
emailAddress: "rod@ejemplo.com",
subject: "Hola",
});
// result.success === true
const bad = notificationSchema.safeParse({
channel: "email",
phoneNumber: "+521234567890", // Error: email necesita emailAddress, no phoneNumber
});
// result.success === falseEsto se integra directo con React Hook Form para formularios dinámicos.
Transforms
válida datos Y transformalos en un solo paso:
// String a número (común con form data)
const priceSchema = z.string()
.transform((val) => parseFloat(val))
.pipe(z.number().min(0, "El precio debe ser positivo"));
priceSchema.parse("19.99"); // 19.99 (number, no string)
priceSchema.parse("-5"); // Error: El precio debe ser positivo
// Normalizar email
const emailSchema = z.string()
.email()
.transform((email) => email.toLowerCase().trim());
emailSchema.parse(" ROD@Ejemplo.COM "); // "rod@ejemplo.com"
// Parsear fecha de string
const dateSchema = z.string()
.transform((str) => new Date(str))
.pipe(z.date());
dateSchema.parse("2026-01-15"); // Date objectEl tipo de entrada y salida pueden ser diferentes. TypeScript lo infiere automáticamente:
type Input = z.input<typeof priceSchema>; // string
type Output = z.output<typeof priceSchema>; // numberCoerción (atajo para transforms comunes)
Zod tiene shortcuts para las transformaciones más comunes:
// En vez de z.string().transform(Number).pipe(z.number())
const age = z.coerce.number().min(18);
age.parse("25"); // 25 (number)
age.parse(25); // 25 (number)
const active = z.coerce.boolean();
active.parse("true"); // true
active.parse(1); // true
active.parse(0); // false
const date = z.coerce.date();
date.parse("2026-01-15"); // Date object
date.parse(1737000000000); // Date objectútil para datos de formularios HTML donde todo llega como string.
superRefine para validaciones cruzadas
Cuando la validación de un campo depende de otro:
const transferSchema = z.object({
fromAccount: z.string(),
toAccount: z.string(),
amount: z.number().positive(),
}).superRefine((data, ctx) => {
if (data.fromAccount === data.toAccount) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "No puedes transferir a la misma cuenta",
path: ["toAccount"],
});
}
});superRefine te da control total sobre los errores: defines el path (que campo marca en rojo), el mensaje, y puedes agregar múltiples issues.
Composición de schemas
No dupliques schemas. Componlos:
// Schema base
const userBase = z.object({
name: z.string().min(2),
email: z.string().email(),
});
// Para crear usuario (todo requerido)
const createUser = userBase.extend({
password: z.string().min(8),
});
// Para actualizar (todo opcional)
const updateUser = userBase.partial();
// Para respuesta de API (sin password, con id)
const userResponse = userBase.extend({
id: z.string(),
createdAt: z.date(),
});
// Solo algunos campos
const userLogin = userBase.pick({ email: true }).extend({
password: z.string(),
});métodos de composición:
.extend()-- agrega campos.partial()-- hace todo opcional.required()-- hace todo requerido.pick({ field: true })-- solo estos campos.omit({ field: true })-- todo menos estos campos.merge(otherSchema)-- combina dos schemas
Patron: schema de API con input/output
// Un solo lugar define la validación de toda una ruta
const createPostEndpoint = {
input: z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).max(5).default([]),
}),
output: z.object({
id: z.string(),
title: z.string(),
slug: z.string(),
createdAt: z.date(),
}),
};
// Validar request
const data = createPostEndpoint.input.parse(await request.json());
// Validar response (útil para tests)
const response = createPostEndpoint.output.parse(result);Este patron es la base de como tRPC funciona. Si te interesa, revisa la guía de tRPC con Next.js.
Siguiente paso
Si usas Zod con formularios, la guía de formularios dinámicos con React Hook Form y Zod aplica estos patrones en la UI. Y para la base de Zod, la guía de validación con Zod cubre todo desde cero.
Preguntas frecuentes
¿Qué es una discriminated union en Zod?
Es un schema donde el tipo de un objeto depende del valor de un campo discriminador. Por ejemplo, si type es 'email', necesitas un campo emailAddress. Si type es 'sms', necesitas phoneNumber. Zod válida automáticamente según el discriminador.
¿Qué hace z.transform()?
Transform te permite modificar los datos después de validarlos. Por ejemplo, validar que un string es un email y luego convertirlo a minusculas. El tipo de salida puede ser diferente al de entrada.
¿Cuál es la diferencia entre refine y superRefine?
refine agrega una validación custom que devuelve true o false. superRefine te da control total: puedes agregar múltiples errores, con paths y mensajes especificos. Usa refine para validaciones simples y superRefine para validaciones cruzadas entre campos.
¿Puedo reutilizar schemas de Zod?
Si. Puedes componer schemas con .merge(), .extend(), .pick(), .omit() y .partial(). Esto te permite crear un schema base y derivar variaciones sin duplicar código.
Articulos relacionados
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.
Formularios dinámicos con React Hook Form y Zod
Crea formularios dinámicos con campos condicionales, arrays de campos y validación type-safe usando React Hook Form y Zod en Next.js.