Zod 4: Validaciones complejas sin volverte loco
Si tu schema de Zod tiene más de tres niveles de anidamiento con condicionales y no estás usando Uniones Discriminadas, estás construyendo una bomba de tiempo tipográfica, no un validador. La complejidad en las validaciones modernas no nace de los tipos de datos simples, sino de las reglas de negocio cruzadas que intentamos forzar en estructuras planas. Zod 4 resuelve este “Specificity Hell” de los datos mediante dos pilares fundamentales: las Uniones Discriminadas para manejar estados excluyentes y el patrón .pipe() para orquestar transformaciones secuenciales sin ensuciar la lógica de negocio.
La trampa de las uniones ambiguas#
Cuando validás un objeto que puede tener diferentes formas (por ejemplo, un pago que puede ser ‘efectivo’ o ‘tarjeta’), usar z.union obliga a Zod a probar cada esquema hasta que uno pase. Esto es lento y genera errores crípticos.
// ❌ Unión simple: Ambigua y lenta
const paymentSchema = z.union([
z.object({ method: z.literal("card"), number: z.string() }),
z.object({ method: z.literal("cash"), amount: z.number() })
]);// ✅ Unión discriminada: Rápida y precisa
const paymentSchema = z.discriminatedUnion("method", [
z.object({ method: z.literal("card"), number: z.string() }),
z.object({ method: z.literal("cash"), amount: z.number() })
]);Con el discriminador "method", Zod sabe exactamente qué esquema usar con solo mirar una propiedad, mejorando el rendimiento y el autocompletado de TypeScript drásticamente.
El patrón .pipe() para validación secuencial#
A veces necesitás transformar un dato antes de validarlo con un esquema complejo. En Zod 3 hacíamos malabares con .transform() y .refine(). En Zod 4, .pipe() permite encadenar esquemas completos como si fueran tuberías de producción.
import { import zz } from "zod";
// 1. Esquema de entrada (input sucio)
const const InputSchema: z.ZodStringInputSchema = import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()._ZodString<$ZodStringInternals<string>>.trim(): z.ZodStringtrim()._ZodString<$ZodStringInternals<string>>.min(minLength: number, params?: string | z.core.$ZodCheckMinLengthParams): z.ZodStringmin(1);
// 2. Esquema de negocio (estructura final)
const const UserSchema: z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>
UserSchema = import zz.function object<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}>(shape?: {
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>
object({
id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodString.uuid(params?: string | z.core.$ZodCheckUUIDParams): z.ZodStringuuid(),
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>
role: import zz.enum<readonly ["admin", "user"]>(values: readonly ["admin", "user"], params?: string | z.core.$ZodEnumParams): z.ZodEnum<{
admin: "admin";
user: "user";
}> (+1 overload)
export enum
enum(["admin", "user"])
});
// 3. La tubería: Parsea string -> JSON -> Valida Objeto
const const Pipeline: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>, z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>>
Pipeline = import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()
.ZodType<any, any, $ZodStringInternals<string>>.transform<any>(transform: (arg: string, ctx: z.core.$RefinementCtx<string>) => any): z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>transform((str: stringstr) => var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): anyConverts a JavaScript Object Notation (JSON) string into an object.parse(str: stringstr))
.ZodType<any, any, $ZodPipeInternals<ZodString, ZodTransform<any, string>>>.pipe<z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>>(target: z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip> | z.core.$ZodType<any, any, z.core.$ZodTypeInternals<any, any>>): z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>, z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>>
pipe(const UserSchema: z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>
UserSchema);
const const result: {
id: string;
role: "admin" | "user";
}
result = const Pipeline: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>, z.ZodObject<{
id: z.ZodString;
role: z.ZodEnum<{
admin: "admin";
user: "user";
}>;
}, z.core.$strip>>
Pipeline.ZodType<any, any, $ZodPipeInternals<ZodPipe<ZodString, ZodTransform<any, string>>, ZodObject<{ id: ZodString; role: ZodEnum<{ admin: "admin"; user: "user"; }>; }, $strip>>>.parse(data: unknown, params?: z.core.ParseContext<z.core.$ZodIssue>): {
id: string;
role: "admin" | "user";
}
parse('{"id": "550e8400-e29b-41d4-a716-446655440000", "role": "admin"}');
Veredicto: ¿Cuándo usar cada uno?#
Estrategias de Validación
Cuándo brilla .pipe()
- Para separar sanitización de validación.
- Cuando el output de un transform necesita validación estructural compleja.
- Para mantener esquemas atómicos y reutilizables.
Cuándo evitarlo
- Si la validación es simple, .refine() es más directo.
- Puede hacer el flujo de datos más difícil de seguir si se abusa.
Dominar estas herramientas en Astro 6 nos permite mantener un Content Layer limpio, donde el esquema no solo valida que los archivos existan, sino que asegura que la integridad del grafo de conocimiento sea indestructible desde el parseo inicial.
La tipado seguro es paz mental.
Podés profundizar en cómo esto afecta al rendimiento en el Edge en nuestro TIL sobre Zod 4 en Edge Runtimes o revisar la guía de migración a Zod 4.