DEV Community

Alex Spinov
Alex Spinov

Posted on

Zod Has a Free API You're Not Using

Zod is the TypeScript-first schema validation library. Most developers use z.object() and z.string() — but Zod ships with powerful APIs that can replace entire validation libraries.

The Free APIs You're Missing

1. z.discriminatedUnion() — Type-Safe Union Parsing

const EventSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
  z.object({ type: z.literal("keypress"), key: z.string(), code: z.number() }),
  z.object({ type: z.literal("scroll"), delta: z.number() }),
]);

type Event = z.infer<typeof EventSchema>;
// Event is automatically: { type: "click"; x: number; y: number } | { type: "keypress"; ... } | ...
Enter fullscreen mode Exit fullscreen mode

40x faster than z.union() — Zod checks the discriminator field first.

2. z.transform() — Parse AND Transform

const DateFromString = z.string()
  .transform((s) => new Date(s))
  .pipe(z.date().min(new Date("2020-01-01")));

const SlugFromTitle = z.string()
  .transform((s) => s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""));

const PriceInCents = z.string()
  .regex(/^\d+\.\d{2}$/)
  .transform((s) => Math.round(parseFloat(s) * 100));

PriceInCents.parse("19.99"); // 1999
Enter fullscreen mode Exit fullscreen mode

3. z.preprocess() — Clean Data Before Validation

const QueryParams = z.object({
  page: z.preprocess((v) => Number(v) || 1, z.number().min(1)),
  limit: z.preprocess((v) => Number(v) || 10, z.number().min(1).max(100)),
  search: z.preprocess((v) => (v === "" ? undefined : v), z.string().optional()),
  tags: z.preprocess(
    (v) => (typeof v === "string" ? v.split(",") : v),
    z.array(z.string()).default([])
  ),
});

QueryParams.parse({ page: "3", limit: "25", search: "", tags: "js,ts" });
// { page: 3, limit: 25, search: undefined, tags: ["js", "ts"] }
Enter fullscreen mode Exit fullscreen mode

4. z.custom() — Domain-Specific Types

const EmailAddress = z.string().email().brand<"Email">();
const UserId = z.string().uuid().brand<"UserId">();
const Money = z.number().positive().brand<"Money">();

type Email = z.infer<typeof EmailAddress>; // string & { __brand: "Email" }
type UserId = z.infer<typeof UserId>; // string & { __brand: "UserId" }

// Now TypeScript prevents mixing up strings!
function sendEmail(to: Email, from: Email) { /* ... */ }
// sendEmail(userId, email) → TypeScript ERROR
Enter fullscreen mode Exit fullscreen mode

5. z.lazy() — Recursive Schemas

interface Category {
  name: string;
  children: Category[];
}

const CategorySchema: z.ZodType<Category> = z.object({
  name: z.string(),
  children: z.lazy(() => CategorySchema.array()),
});

// Validates deeply nested tree structures
CategorySchema.parse({
  name: "Electronics",
  children: [
    { name: "Phones", children: [
      { name: "iPhone", children: [] },
    ]},
  ],
});
Enter fullscreen mode Exit fullscreen mode

Getting Started

npm install zod
Enter fullscreen mode Exit fullscreen mode

Need data from any website delivered as clean JSON? I build production web scrapers that handle anti-bot, proxies, and rate limits. 77 scrapers running in production. Email me: Spinov001@gmail.com

Check out my awesome-web-scraping list for the best scraping tools and resources.

Top comments (0)