Validating that a field exists is the floor, not the ceiling. Your CMS or API data is often "dirty": dates as strings, empty tags, non-normalized slugs. If you clean this up in your components, you're duplicating logic that Zod 4 can solve in a single schema.
In this guide, we'll see how to transform your validation into a pure data pipeline.
From Validator to Transformation Pipeline
Look at the difference between a schema that only validates and one that transforms, normalizes, and performs cross-field validation:
// Advanced Schema — Validate + Transform + Normalize
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: "Date cannot be in the future",
path: ["publishedAt"],
});
}
});
The Three Patterns You Need to Know
1. .transform() — Modify data after validation
Change the output type directly in the schema.
2. .superRefine() — Cross-field validation
Perfect for complex rules like "Published articles require a date, but drafts don't".
3. .prefault() — The new Zod 4 pre-parse default
In Zod 4, .default() applies after the transform. Use .prefault() if you need the default value to pass through your transformation pipeline (Zod 3 behavior).
Where should the logic go?
- In the Schema (Zod): Single source of truth, automatic output typing, zero boilerplate in UI.
- In Components: Dispersed logic, harder to test, and redundant.
Validating is easy. Transforming with elegance is the art of a Senior Engineer.
Originally published at: campa.dev
Top comments (0)