This page covers the handful of concepts that make up almost all day-to-day Zod usage. Every claim links back to code in this repository.

Build a schema, then parse

A schema is a value you construct once and reuse. z.string(), z.number(), z.object({...}) and about forty other factories create them; methods like .min() return new schemas with checks attached. Zod's own test suite is the cleanest demonstration:

packages/zod/src/v4/classic/tests/string.test.ts
const minFive = z.string().min(5, "min5");
const maxFive = z.string().max(5, "max5");
const justFive = z.string().length(5);
const nonempty = z.string().min(1, "nonempty");

test("length checks", () => {
  minFive.parse("12345");
  minFive.parse("123456");
  // …
  expect(() => minFive.parse("1234")).toThrow();
  expect(() => maxFive.parse("123456")).toThrow();
});

.parse(data) returns the validated (and possibly transformed) value, typed as the schema's output type. On failure it throws a ZodError.

safeParse: no exceptions

When invalid input is expected — which is the whole point of validation — use .safeParse(). It returns a discriminated union instead of throwing:

packages/zod/src/v4/classic/parse.ts
export type ZodSafeParseResult<T> = ZodSafeParseSuccess<T> | ZodSafeParseError<T>;
export type ZodSafeParseSuccess<T> = { success: true; data: T; error?: never };
export type ZodSafeParseError<T> = { success: false; data?: never; error: ZodError<T> };

Checking result.success narrows the type: in the true branch result.data is fully typed, in the false branch result.error holds the issue list. Async variants (parseAsync, safeParseAsync) exist for schemas containing async refinements; a sync parse on such a schema throws $ZodAsyncError with the message "Encountered Promise during synchronous parse. Use .parseAsync() instead." (packages/zod/src/v4/core/core.ts).

One schema, two static types

z.infer<typeof schema> extracts the output type; z.input and z.output exist for schemas whose transform changes the type. These are aliases defined in the core:

packages/zod/src/v4/core/core.ts
export type input<T> = T extends { _zod: { input: any } } ? T["_zod"]["input"] : unknown;
export type output<T> = T extends { _zod: { output: any } } ? T["_zod"]["output"] : unknown;

export type { output as infer };

Refine, transform, compose

Beyond built-in checks, .refine(fn, message) attaches an arbitrary predicate, .transform(fn) maps the value after validation, and .or() / .and() / .pipe() compose schemas. Wrappers handle absence: .optional(), .nullable(), .default(value), .catch(fallback).

Coercion for stringly-typed input

The z.coerce namespace builds schemas that convert before validating — the standard tool for process.env and query strings:

packages/zod/src/v4/classic/coerce.ts
export function number<T = unknown>(params?: string | core.$ZodNumberParams): ZodCoercedNumber<T> {
  return core._coercedNumber(schemas.ZodNumber, params) as ZodCoercedNumber<T>;
}

For strings this sets coerce: true on the schema definition, and the string parser then runs payload.value = String(payload.value) before the type check (packages/zod/src/v4/core/schemas.ts).

Where next

Sources: packages/zod/src/v4/classic/parse.ts, packages/zod/src/v4/classic/tests/string.test.ts, packages/zod/src/v4/classic/coerce.ts · last synced 2026-08-10 · 2d90846 · version 4.4.3