Use Zod .transform() and .default() with JSON Schemas
Learn how to use Zod's .transform() for data coercion and .default() for fallback values when converting JSON to validated, typed data.
Detailed Explanation
Transforms and Defaults in Zod
Zod is not just a validator — it can also transform data during parsing. This makes it ideal for normalizing messy API responses into clean, typed data.
.default() — Fallback Values
import { z } from "zod";
const configSchema = z.object({
theme: z.enum(["light", "dark"]).default("dark"),
pageSize: z.number().int().default(20),
showSidebar: z.boolean().default(true),
locale: z.string().default("en"),
});
type Config = z.infer<typeof configSchema>;
// Missing fields get defaults
configSchema.parse({});
// { theme: "dark", pageSize: 20, showSidebar: true, locale: "en" }
configSchema.parse({ theme: "light" });
// { theme: "light", pageSize: 20, showSidebar: true, locale: "en" }
.transform() — Data Coercion
const userSchema = z.object({
name: z.string().transform((s) => s.trim()),
email: z.string().email().transform((s) => s.toLowerCase()),
createdAt: z.string().datetime().transform((s) => new Date(s)),
tags: z.string().transform((s) => s.split(",").map((t) => t.trim())),
});
userSchema.parse({
name: " Alice ",
email: "ALICE@Example.COM",
createdAt: "2024-03-15T10:30:00Z",
tags: "dev, design, pm",
});
// {
// name: "Alice",
// email: "alice@example.com",
// createdAt: Date object,
// tags: ["dev", "design", "pm"]
// }
Input vs Output Types
When using transforms, the input and output types differ:
const schema = z.string().transform((s) => parseInt(s, 10));
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // number (same as z.infer)
Use z.input for form/API types and z.output (or z.infer) for internal types.
.preprocess() — Transform Before Validation
const numberFromString = z.preprocess(
(val) => (typeof val === "string" ? Number(val) : val),
z.number().min(0).max(100)
);
numberFromString.parse("42"); // 42
numberFromString.parse(42); // 42
numberFromString.parse("abc"); // ZodError (NaN fails z.number())
z.coerce — Built-in Coercion
// Simpler alternative to .preprocess()
z.coerce.number().parse("42"); // 42
z.coerce.boolean().parse("true"); // true
z.coerce.date().parse("2024-03-15T10:30:00Z"); // Date object
z.coerce.string().parse(42); // "42"
Chaining Transforms
const slugSchema = z.string()
.min(1)
.transform((s) => s.toLowerCase())
.transform((s) => s.replace(/\s+/g, "-"))
.transform((s) => s.replace(/[^a-z0-9-]/g, ""));
slugSchema.parse("Hello World!"); // "hello-world"
Transforms turn Zod from a passive validator into an active data processing pipeline that validates and normalizes in a single step.
Use Case
You receive data from multiple third-party APIs with inconsistent formatting (mixed case emails, strings instead of numbers, missing optional fields) and need a Zod schema that normalizes everything into a consistent internal format.