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
{}

Type Inference Gotcha: Coercion

Both schemas below infer the exact same TypeScript type: { count: number }. But at runtime they behave very differently. z.number() only accepts an actual number. z.coerce.number() wraps the input in Number() first, so a string like "42" becomes 42 before validation runs. The input starts as a string, and notice how it parses successfully. Try changing "42" to "abc" to see coercion fail, or remove the quotes to see both schemas accept it. The Gotcha is that null, true, false, [], and other weird values all coerce to a number without throwing an error, which can lead to surprising results if you use coercion without realizing it.

Schema
import { z } from 'zod';

// Fails for "42". The 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 identical type:
type StrictSchema = {
  count: number; // only accepts actual numbers at runtime
};

type CoercedSchema = {
  count: number; // accepts "42", true, false, [], null at runtime
};
Output
{
  "count": 42
}