This document explains how to add custom validation logic to Zod schemas using refinements. Refinements allow you to define validation rules beyond the built-in constraints, enabling complex business logic validation, cross-field validation, and custom error messages.
For information about data transformations that modify values, see Transformations and Preprocessing. For information about schema composition methods, see Schema Composition.
Refinements extend Zod's validation capabilities by allowing you to define custom validation functions that execute after the schema's base type and constraint checks pass. This document covers:
.refine() method for adding simple custom validation.superRefine() / .check() methods for complex validation with fine-grained controlSources: packages/zod/src/v4/core/checks.ts11-39 packages/zod/src/v4/core/util.ts803-824
.refine()The .refine() method adds a validation function to any schema. It receives the validated value and returns a boolean indicating whether validation passed.
| Option | Type | Description |
|---|---|---|
message | string | Static error message |
params | object | Additional data attached to the issue |
path | PropertyKey[] | Custom path for the error (useful in object refinements) |
Example with params:
Sources: packages/zod/src/v4/classic/tests/error.test.ts62-82 packages/zod/src/v4/classic/tests/error.test.ts126-142
.superRefine() and .check()For complex validation requiring multiple issues or conditional logic, use .superRefine() (Classic API) or .check() (Mini API). These methods provide a context object for fine-grained control.
| Method/Property | Type | Description |
|---|---|---|
ctx.value | T | The validated input value |
ctx.issues | $ZodRawIssue[] | Array of issues to be reported |
ctx.addIssue(issue) | void | Helper to add an issue (accepts string or object) |
ctx.addIssue(string) | void | Shorthand for adding a simple custom message |
Sources: packages/zod/src/v4/classic/tests/recursive-types.test.ts449-509 packages/zod/src/v4/classic/tests/error.test.ts276-294 packages/zod/src/v4/classic/tests/preprocess.test.ts27-45
The abort flag controls whether validation continues after a check fails. By default, Zod continues executing all checks to collect all validation errors.
The validation system uses utility functions to determine if validation should continue:
| Function | Purpose | File Location |
|---|---|---|
util.aborted() | Checks if any non-continuable issue exists | packages/zod/src/v4/core/util.ts804-812 |
util.explicitlyAborted() | Checks for explicit abort (continue === false) | packages/zod/src/v4/core/util.ts816-824 |
Sources: packages/zod/src/v4/core/checks.ts11-18 packages/zod/src/v4/core/util.ts803-824
whenThe when property on check definitions allows conditional execution of validation logic.
The when function receives the parse payload and returns true if the check should execute.
If no when function is provided, most checks default to a function that checks if validation has already been aborted:
Sources: packages/zod/src/v4/core/checks.ts11-18 packages/zod/src/v4/core/checks.ts460-463
Refinements can be asynchronous for validation requiring I/O operations like database queries or API calls.
| Method | Supports Async Refinements |
|---|---|
.parse() | ❌ No (throws if async detected) |
.safeParse() | ❌ No (throws if async detected) |
.parseAsync() | ✅ Yes |
.safeParseAsync() | ✅ Yes |
.spa() | ✅ Yes (alias for safeParseAsync) |
Sources: README.md124-131 packages/zod/src/v4/classic/tests/index.test.ts480-489
The finalizeIssue function resolves error messages through a precedence chain:
.error() function.error() function (parse options)customError() from configlocaleError())Use the path option to direct errors to specific fields:
Sources: packages/zod/src/v4/core/util.ts838-864 packages/zod/src/v4/classic/tests/error.test.ts205-230 packages/zod/src/v4/classic/tests/error.test.ts276-294
| Feature | Purpose | When to Use | Code Entity |
|---|---|---|---|
| Refinements | Custom validation logic | Complex business rules, cross-field validation | $ZodRefined |
| Transforms | Modify output values | Data conversion, formatting | $ZodTransform |
| Checks | Built-in constraints | Standard validation (min, max, regex) | $ZodCheck |
| Preprocessing | Modify input before validation | Input sanitization, normalization | $ZodTransform (pre) |
Sources: Based on validation pipeline architecture
Note that .pick(), .omit(), and .partial() cannot be used on object schemas with refinements:
Sources: packages/zod/src/v4/classic/tests/object.test.ts605-630 packages/zod/src/v4/classic/tests/pickomit.test.ts138-158
The check system provides the low-level infrastructure that refinements build upon:
$ZodCheckDef: Base interface for check definitions including abort and when options$ZodCheckInternals: Contains the check function and metadata$ZodCheck: The check instance attached to schemasRefinements internally create checks with code: "custom":
Sources: packages/zod/src/v4/core/checks.ts11-39 packages/zod/src/v4/core/util.ts903-918
Refinements provide Zod's extensibility mechanism for custom validation:
.refine() for simple boolean validation with custom messages.superRefine() / .check() for complex multi-issue validation with path controlThe refinement system integrates seamlessly with Zod's type inference, maintaining type safety while allowing arbitrary validation logic.
Refresh this wiki