Zod Showcase

Common Pitfalls

Mistakes that trip up almost every Zod beginner. Understanding these early saves you hours of debugging later.

.parse vs .safeParse

.parse() throws a ZodError on invalid input. If it is not caught, your app crashes. .safeParse() always returns a result object. The output below uses safeParse. Try making the input valid to see the success path.

Schema
import { z } from 'zod';

const schema = z.object({ name: z.string(), age: z.number() });

// ❌ Throws on invalid input — crashes if uncaught
const data = schema.parse(input);

// ✅ Always returns { success, data | error }
const result = schema.safeParse(input);
if (result.success) {
  console.log(result.data);   // typed and safe
} else {
  console.log(result.error);  // ZodError with issues
}
Inferred Type
// safeParse return type:
type SafeParseResult<T> =
  | { success: true;  data: T }
  | { success: false; error: ZodError }
Output
  • age Invalid input: expected number, received string

partial() vs Required Fields

.partial() makes every field optional, which is useful for PATCH endpoints but easy to misapply when you need all fields. The schema below uses .partial(). Try submitting an empty object {}. It passes even though the original schema requires name, email, and age.

Schema
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  email: z.email(),
  age: z.number(),
});

// ❌ Common mistake: using .partial() for a full create
const CreateUserSchema = UserSchema.partial();
// All fields are now optional — {} is valid!

// ✅ Use .partial() only for updates (PATCH)
const UpdateUserSchema = UserSchema.partial();
// ✅ Use the original for creates (POST)
const FullUserSchema = UserSchema;
Inferred Type
// After .partial():
type PartialUser = {
  name?: string | undefined;
  email?: string | undefined;
  age?: number | undefined;
}
Output
{}

z.record() Requires Two Arguments

In Zod v3, z.record(z.string()) created a string-keyed, string-valued record. In Zod v4 the single-argument form was dropped. You must now pass both a key schema and a value schema. This trips up a lot of people migrating from v3. Try setting a value to a number to see the type error.

Schema
import { z } from 'zod';

// ❌ Zod v3 — no longer valid in Zod v4
const OldSchema = z.record(z.string());

// ✅ Zod v4 — always specify key AND value schema
const schema = z.record(z.string(), z.string());

// Works with any key/value types:
const ScoreMap = z.record(z.string(), z.number());
Inferred Type
// z.record(z.string(), z.string()):
type Schema = Record<string, string>

// z.record(z.string(), z.number()):
type ScoreMap = Record<string, number>
Output
{
  "name": "Alice",
  "role": "admin"
}

Type Inference Gotcha: Coercion

The inferred type IS the schema. If the schema says z.number(), passing a numeric string "42" fails, even though it looks like a number. This comes up a lot with form data and query params, which are always strings. Use z.coerce.number() to handle this automatically.

Schema
import { z } from 'zod';

// ❌ Fails for "42" — schema expects a number, not a string
const StrictSchema = z.object({
  count: z.number(),
});

// ✅ Coerces "42" → 42 before validating
const CoercedSchema = z.object({
  count: z.coerce.number(),
});

// z.coerce wraps the value in Number(), String(), etc.
// Use it when input comes from FormData, URLSearchParams,
// or any source that serializes everything as strings.
Inferred Type
// Both schemas infer the same type:
type Schema = {
  count: number;
}
// But z.coerce.number() accepts "42" at runtime.
// z.number() does not.
Output
  • count Invalid input: expected number, received string