TypeScript-first schema validation: install, define schemas, parse vs safeParse, error formatting, and what changed in v4.
Install
Zod v4 (currently 4.6.x) is what npm install zod gives you today — there's no separate "v4" package to opt into.
Import
The docs use the namespace import throughout v4 — it's also what makes the tree-shakable Zod Mini variant possible (see the v4 tab).
Define a schema, infer the type
The type is derived, never hand-written — rename or add a field on the schema and the type updates itself.
Validate: .parse() vs .safeParse()
.parse() is for data you trust enough to want a hard failure on; .safeParse() is for anything you need to handle gracefully — most form and API input.
Schema type reference
Every primitive, string format, and composite type Zod ships, plus the object-schema methods you'll reach for constantly. String-format validators (email, URL, UUID, ...) became top-level functions in v4 — the old chained-method style (z.string().email()) still parses but doesn't tree-shake as well.
| Type | Syntax | Notes |
|---|---|---|
| String | z.string() | UTF-16 string of any length |
| Number | z.number() | finite numbers only — excludes NaN and Infinity |
| BigInt | z.bigint() | |
| Boolean | z.boolean() | |
| Date | z.date() | must be a real Date instance, not a date string |
| Symbol | z.symbol() | |
| Undefined | z.undefined() | |
| Null | z.null() | |
| Void | z.void() | alias for z.undefined() — function return values |
| Any | z.any() | disables type checking entirely |
| Unknown | z.unknown() | safer than any — narrow it before use |
| Never | z.never() | rejects every value |
z.email() | top-level in v4, replacing z.string().email() | |
| URL | z.url() | WHATWG URL parser |
| UUID | z.uuid() / z.uuidv4() / z.uuidv7() | z.uuid({ version: "v4" }) for one specific version |
| ISO date | z.iso.date() | YYYY-MM-DD |
| ISO datetime | z.iso.datetime() | |
| IPv4 / IPv6 | z.ipv4() / z.ipv6() | |
| JWT | z.jwt() | |
| Base64 | z.base64() / z.base64url() | |
| Hex | z.hex() | |
| Hash | z.hash("sha256") | md5, sha1, sha256, sha384, or sha512 |
| Object | z.object({ ... }) | |
| Strict object | z.strictObject({ ... }) | unknown keys fail validation |
| Loose object | z.looseObject({ ... }) | unknown keys pass through untouched |
| Array | z.array(schema) | |
| Tuple | z.tuple([a, b]) | fixed length, a type per position |
| Union | z.union([a, b]) | logical OR — tries each option in order |
| Discriminated union | z.discriminatedUnion("key", [a, b]) | faster and clearer errors than a plain union |
| Record | z.record(keySchema, valSchema) | |
| Map / Set | z.map(k, v) / z.set(schema) | |
| Enum | z.enum(["a", "b"]) | also accepts a TypeScript enum |
| Literal | z.literal("value") | z.literal([a, b]) for multiple literals |
| Intersection | z.intersection(a, b) | logical AND |
| Extend | .extend({ field: schema }) | add fields or override existing ones |
| Pick / Omit | .pick({ field: true }) / .omit({ field: true }) | |
| Partial | .partial() | every field becomes optional |
| Required | .required() | every field becomes required |
| Strict | .strict() | reject unknown keys — same as z.strictObject |
| Passthrough | .passthrough() | keep unknown keys — same as z.looseObject |
| Keyof | .keyof() | the object's own keys, as a ZodEnum |
Constraints, coercion & custom logic
Built-in constraint methods cover most cases; .refine(), .superRefine(), .transform(), and .pipe() cover the rest.
| Applies to | Method | Meaning |
|---|---|---|
| String | .min(n) / .max(n) / .length(n) | length bounds, in Unicode code points |
| String | .nonempty() | alias for .min(1) |
| String | .regex(pattern) | must match a regular expression |
| String | .startsWith() / .endsWith() / .includes() | substring checks |
| String | .trim() / .toLowerCase() / .toUpperCase() | normalise the value before validating |
| Number | .gt(n) / .gte(n) | greater than / or equal — .min() is an alias for .gte() |
| Number | .lt(n) / .lte(n) | less than / or equal — .max() is an alias for .lte() |
| Number | .positive() / .negative() | shorthand for .gt(0) / .lt(0) |
| Number | .nonnegative() / .nonpositive() | shorthand for .gte(0) / .lte(0) |
| Number | .multipleOf(n) | must be evenly divisible by n — alias .step() |
| Number | z.int() / z.int32() | integer, optionally restricted to the int32 range |
| Array / Set | .min(n) / .max(n) / .length(n) | item-count bounds |
| Array | .nonempty() | require at least one item |
| Any schema | .optional() / .nullable() / .nullish() | allow undefined / null / both |
| Any schema | .default(value) | fill in when the input is undefined |
| Any schema | .catch(value) | fall back to a value instead of failing |
| Coercion | z.coerce.string() | calls String(input) before validating |
| Coercion | z.coerce.number() | calls Number(input) before validating |
| Coercion | z.coerce.boolean() | calls Boolean(input) before validating |
.refine() — one custom rule
Runs after the built-in checks pass. Return a boolean; fail with the error option.
.transform() — change the output
Validation and transformation in one step; the inferred type reflects the transformed value, not the input.
.pipe() — chain two schemas
Validate as one type, then feed the result into a second schema. Clearer errors than a single do-everything coercion.
.superRefine() — cross-field / multi-issue
Full control over the issue list via ctx.addIssue() — the only way to validate one field against another.
Reading and formatting errors
A failed safeParse() gives you a ZodError with a flat .issues array — three built-in functions reshape it for different audiences.
The raw issues array
Every other formatter below is built from this. Each issue carries a code, a path to the offending field, a message, and check-specific detail (e.g. expected).
z.prettifyError() — for a log or console
A human-readable multi-line string. Good for CLI tools and server logs, not for a UI.
z.treeifyError() — nested, mirrors the schema
The current recommended way to walk errors for a nested form. Replaces the deprecated z.formatError().
z.flattenError() — flat form errors
For a single-level schema (most login/signup forms): one array for whole-form errors, one object of per-field arrays.
error param- v4 replaced four separate options (
message,invalid_type_error,required_error,errorMap) with a singleerrorparam that accepts a string or a function — a string or function passed directly is shorthand for it. - Precedence, highest to lowest: a check-level error (
.min(5, "Too short!")) → a schema-level error (z.string("Invalid name")) → a per-parse error passed to.parse(data, { error })→ the global error map (z.config()) → locale defaults.
- Set once, applies to every schema that doesn't override it locally.
What changed in v4, and what to reach for next
v4 is a from-scratch rewrite of Zod's internals, not an incremental release — the numbers below are from Zod's own benchmarks against v3.
String parsing is 14.71x faster, array parsing 7.43x, object parsing 6.5x, versus v3. The core bundle shrank about 57% (2.3x smaller), and chaining methods like .extend()/.omit() produces roughly 100x fewer tsc type instantiations — the thing that made large v3 schemas slow down your editor.
Same validation logic, a functional API instead of method chaining — trades some discoverability for a smaller tree-shaken bundle. About 64% smaller for a simple schema (2.12kb vs 5.91kb gzipped), widening further on complex object schemas.
Method memoization defers allocating bound methods until they're actually called. A bare z.string() went from 7.5kb to 784 bytes of retained heap — worth knowing if you build a lot of schemas you don't always exercise.
Prefer the top-level functions — they tree-shake; the old chained-method spelling still works but won't be trimmed from your bundle if unused elsewhere.
Memoization made chaining cheap at definition time, but a schema built inside a hot request handler or render function still redoes that work on every call. Define once at module scope, reuse everywhere.
API: tRPC, nestjs-zod, Express Zod API. Forms: Superforms, conform, zod-validation-error. Zod → other formats: zod-openapi, prisma-zod-generator. Other formats → Zod: orval, Hey API. Full list on the Ecosystem page (Docs tab).
Official docs & further reading
The primary source is zod.dev, written and maintained by Zod's author. Every claim on this page can be checked against one of these — and Zod moves fast, so when this page and the docs disagree, the docs win.