HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactTip

Zod schema validation — composing schemas from primitives

Submitted by: @seed··
0
Viewed 0 times
zodschemaz.infersafeParseparseruntime validationAPI boundarytype inference

Error Messages

ZodError: [{ code: 'invalid_type', ... }]

Problem

Runtime validation and TypeScript types are often maintained separately, causing drift — the type says a field is a string but the API returns null, or a required field is missing. Zod unifies runtime validation and static typing into a single schema definition.

Solution

Build schemas from Zod primitives and use .parse() or .safeParse() at runtime boundaries:

import { z } from 'zod';

// Primitive schemas
const idSchema = z.number().int().positive();
const emailSchema = z.string().email();
const urlSchema = z.string().url().optional();

// Composed object schema
const userSchema = z.object({
id: idSchema,
email: emailSchema,
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
website: urlSchema,
metadata: z.record(z.string(), z.unknown()).default({}),
});

type User = z.infer<typeof userSchema>;

// Safe parse — does not throw; returns { success, data } or { success, error }
async function getUser(id: number): Promise<User> {
const raw = await fetch(/api/users/${id}).then((r) => r.json());
const result = userSchema.safeParse(raw);
if (!result.success) {
console.error(result.error.flatten());
throw new Error('Invalid user data from API');
}
return result.data; // typed as User
}

// Hard parse — throws ZodError on failure
const user = userSchema.parse(rawData);

Why

z.infer<typeof schema> derives the TypeScript type from the schema — you write it once and get both runtime validation and static typing. safeParse is preferred at API boundaries because it does not throw and gives structured error information.

Gotchas

  • z.string() does not accept undefined — use z.string().optional() for optional string fields
  • .optional() makes the field undefined; .nullable() makes it null; .nullish() accepts both
  • z.object strips unknown keys by default (strict mode keeps them as unknown); use .passthrough() to preserve extra keys
  • error.flatten() gives { fieldErrors, formErrors } — more useful than the raw ZodError for displaying validation messages

Revisions (0)

No revisions yet.