Skip to main content

Overview

Refinements allow you to add custom validation logic beyond Zod’s built-in validators. While transformations modify data, refinements validate data without changing it.

Basic Refinement with .refine()

Use .refine() to add custom validation:
From packages/zod/src/v4/classic/schemas.ts:92-95, the .refine() signature:

Custom Error Messages

Provide custom error messages:
From packages/zod/src/v4/classic/tests/refine.test.ts:54-75:

Refine on Objects

Refinements are powerful for cross-field validation:
From packages/zod/src/v4/classic/tests/refine.test.ts:169-181:

Error Path

The path option specifies where the error should appear:

Async Refinements

Refinements can be asynchronous for database checks, API calls, etc:
From packages/zod/src/v4/classic/tests/refine.test.ts:77-108:
Async refinements require parseAsync() or safeParseAsync(). Using parse() will throw an error.

superRefine() for Advanced Validation

For complex validation with multiple errors, use .superRefine():
From packages/zod/src/v4/classic/schemas.ts:96-98, the .superRefine() signature:
From packages/zod/src/v4/classic/tests/refine.test.ts:183-215:

Adding Multiple Issues

.superRefine() allows reporting multiple validation errors:

Early Termination

Control validation flow with early termination options:

Using fatal: true

From packages/zod/src/v4/classic/tests/refine.test.ts:133-154:

Using continue: false

Using abort in .refine()

From packages/zod/src/v4/classic/tests/refine.test.ts:155-167:

Type Narrowing with Refinements

Refinements can narrow TypeScript types using type predicates:
From packages/zod/src/v4/classic/tests/refine.test.ts:422-432:

Practical Examples

Email Uniqueness Check

Password Confirmation

Date Range Validation

Complex Business Logic

Credit Card Validation

Refinements vs Transformations

Key Difference:
  • Refinements validate data without changing it
  • Transformations modify data after validation

Chaining Refinements

Multiple refinements can be chained:

Performance Considerations

Async refinements can be slow. Consider caching or batching database lookups for better performance.

Best Practices

  1. Use built-in validators first - They’re optimized and well-tested
  2. Keep refinements focused - One validation per refinement
  3. Use .superRefine() for multiple errors - Better UX than stopping at first error
  4. Set appropriate error paths - Help users fix the right field
  5. Consider async performance - Cache or batch when possible
  6. Use type predicates for narrowing - Get better TypeScript types

Common Patterns

Conditional Validation

Dependent Fields

Next Steps