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.

Advanced

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.

Try It — JSON to Zod Schema

Open full tool