Zod 4: Esquemas Dinámicos y Transformaciones Complejas
Validar que un campo existe es el piso, no el techo. Los datos de tu CMS vienen sucios: fechas como strings, tags vacíos, slugs sin normalizar. Si limpiás todo eso en los componentes, estás duplicando lógica que Zod 4 puede resolver en un solo schema.
De validador a pipeline de transformación#
Mirá la diferencia entre un schema que solo valida y uno que transforma, normaliza y valida cross-field:
// Schema básico — solo valida tipos
const blogSchema = z.object({
title: z.string(),
slug: z.string(),
publishedAt: z.coerce.date(),
tags: z.array(z.string()),
});
// Resultado: { title: " Mi Post ", slug: "Mi Post", tags: ["", "css"] }// Schema avanzado — valida + transforma + normaliza
const blogSchema = z.object({
title: z.string().trim(),
slug: z.string()
.transform((s) => s.toLowerCase().replace(/\s+/g, "-")),
publishedAt: z.string()
.transform((s) => new Date(s)),
tags: z.array(z.string())
.transform((arr) => arr.filter(Boolean)),
}).superRefine((data, ctx) => {
if (data.publishedAt > new Date()) {
ctx.addIssue({
code: "custom",
message: "La fecha no puede ser futura",
path: ["publishedAt"],
});
}
});
// Resultado: { title: "Mi Post", slug: "mi-post", tags: ["css"] }Los tres patrones que necesitás conocer#
.transform() — Modifica datos después de validar. Cambia el tipo de output:
// String → Date
const DateField = z.string().transform((s) => new Date(s));
// String → slug limpio
const SlugField = z.string().transform((s) =>
s
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, ""),
);
// Array → sin vacíos
const TagsField = z
.array(z.string())
.transform((arr) => arr.filter((t) => t.trim().length > 0));.superRefine() — Validación cross-field con múltiples errores:
const articleSchema = z
.object({
title: z.string(),
draft: z.boolean(),
publishedAt: z.date().optional(),
})
.superRefine((data, ctx) => {
// Un borrador no necesita fecha, pero un publicado sí
if (!data.draft && !data.publishedAt) {
ctx.addIssue({
code: "custom",
message: "Artículo publicado requiere fecha",
path: ["publishedAt"],
});
}
});.prefault() — El nuevo .default() de Zod 4 para valores pre-transformación:
// Zod 4: .default() espera valor del OUTPUT (número)
const schema = z
.string()
.transform((s) => s.length)
.default(0);
// Zod 4: .prefault() espera valor del INPUT (string)
const schema = z
.string()
.transform((s) => s.length)
.prefault("tuna"); // → 4¿Dónde va la lógica de transformación?
En el schema (Zod)
- Single source of truth para datos
- Tipado automático del output (z.infer)
- Zero boilerplate en componentes
- Validación + transformación en un solo paso
En los componentes
- Transforms complejos dificultan el debugging
- Async transforms requieren parseAsync()
- Lógica de negocio pesada no pertenece al schema
Validar es fácil. Transformar con elegancia es el arte.
Si querés ver cómo migrar de Zod 3 a Zod 4 en Astro 6, empezá por la guía de migración del Content Layer. Y si necesitás validación ultra-estricta para colecciones dinámicas, mirá cómo usar .strict() en Live Collections.