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 validacion basica, estos patrones avanzados te resuelven los casos reales que aparecen en produccion: formularios que cambian segun selecciones, datos que necesitan transformacion, y schemas que se componen entre si.
Si necesitas la base primero, arranca con la guia de Zod para validacion.
Discriminated Unions
El patron mas util para formularios dinamicos. 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 dinamicos.
Transforms
Valida datos Y transformalos en un solo paso:
// String a numero (comun 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 automaticamente:
type Input = z.input<typeof priceSchema>; // string
type Output = z.output<typeof priceSchema>; // numberCoercion (atajo para transforms comunes)
Zod tiene shortcuts para las transformaciones mas 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 objectUtil para datos de formularios HTML donde todo llega como string.
superRefine para validaciones cruzadas
Cuando la validacion 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 multiples issues.
Composicion 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(),
});Metodos de composicion:
.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 validacion 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 (util para tests)
const response = createPostEndpoint.output.parse(result);Este patron es la base de como tRPC funciona. Si te interesa, revisa la guia de tRPC con Next.js.
Siguiente paso
Si usas Zod con formularios, la guia de formularios dinamicos con React Hook Form y Zod aplica estos patrones en la UI. Y para la base de Zod, la guia de validacion con Zod cubre todo desde cero.
Preguntas frecuentes
Que 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 valida automaticamente segun el discriminador.
Que hace z.transform()?
Transform te permite modificar los datos despues 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.
Cual es la diferencia entre refine y superRefine?
refine agrega una validacion custom que devuelve true o false. superRefine te da control total: puedes agregar multiples 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 codigo.
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. Verificacion de firmas, tipado y manejo de errores.
Formularios Dinamicos con React Hook Form y Zod
Crea formularios dinamicos con campos condicionales, arrays de campos y validacion type-safe usando React Hook Form y Zod en Next.js.