Sugerencias
← TIL
~2 min de lectura
#astro-6#zod-4#typescript#content-layer

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:

Refactor
ts
// 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:

transform-patterns.ts
// 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:

superrefine-cross-field.ts
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:

prefault-vs-default.ts
// 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.

Enlace copiado al portapapeles