Joi was built for JavaScript and lacks native TypeScript type inference.
Zod is TypeScript-first, providing automatic type inference with z.infer,
a smaller bundle size, and first-class support in modern frameworks like tRPC,
React Hook Form, and Next.js. This guide covers every step of the migration.
Install SchemaShift and run the migration in two commands:
npm install -g schemashift-cli
schemashift migrate ./src -f joi -t zod
This analyzes every file in ./src, rewrites Joi schemas to Zod,
updates imports, and creates backup files automatically.
import Joi from 'joi';
const userSchema = Joi.object({
name: Joi.string().required().min(2),
email: Joi.string().email().required(),
port: Joi.number().port(),
role: Joi.string().valid('admin', 'user')
});
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
port: z.number().int().min(0).max(65535),
role: z.enum(['admin', 'user'])
});
type User = z.infer<typeof userSchema>;
Notice how Zod gives you the User type for free — no separate
interface to maintain.
Complete mapping from Joi methods to their Zod equivalents. SchemaShift handles all of these automatically.
| Joi | Zod | Notes |
|---|---|---|
Joi.string() |
z.string() |
|
Joi.number() |
z.number() |
|
Joi.boolean() |
z.boolean() |
|
Joi.date() |
z.date() |
|
Joi.any() |
z.unknown() |
Prefer z.unknown() over z.any() for type safety |
Joi.binary() |
z.instanceof(Buffer) |
Node.js only |
.required() |
(removed) | Zod fields are required by default |
.optional() |
.optional() |
|
.allow(null) |
.nullable() |
|
.forbidden() |
z.never() |
|
.email() |
.email() |
|
.uri() |
.url() |
|
.uuid() |
.uuid() |
|
.ip() |
.ip() |
|
.pattern(regex) |
.regex(regex) |
|
.alphanum() |
.regex(/^[a-zA-Z0-9]+$/) |
No built-in Zod equivalent |
.greater(n) |
.gt(n) |
|
.less(n) |
.lt(n) |
|
.port() |
.int().min(0).max(65535) |
No built-in port validator in Zod |
.valid(...) |
z.enum([...]) |
For string literals |
.invalid(...) |
.refine() |
Custom validation needed |
.custom(fn) |
.refine(fn) |
|
Joi.alternatives() |
z.union() |
These Joi patterns have no direct Zod equivalent and require manual attention. SchemaShift generates scaffolding code and inline TODO comments for each one.
.when() conditionals —
Joi's .when() supports conditional schema selection based on sibling fields.
Zod has no built-in equivalent. SchemaShift generates .superRefine()
scaffolding with the condition logic preserved in comments.
.with() / .without() —
Field dependency rules (e.g., "if field A is present, field B is required") map to
.superRefine() with manual field-presence checks in Zod.
.and() / .nand() / .or() / .xor() —
Group constraints on object fields require custom .superRefine() logic.
SchemaShift generates the boilerplate with field names preserved.
Joi.ref() cross-references —
Cross-field references like Joi.ref('password') for confirmation fields
become .refine() callbacks that access the full object via the path.
.messages() custom error strings —
Joi's centralized .messages() object does not map cleanly.
Zod uses inline error strings passed directly to each validation method
(e.g., .min(2, { message: "Too short" })).
.external() async validation —
Joi's .external() for async checks becomes
.refine(async () => { ... }) in Zod, requiring
.parseAsync() instead of .parse() at the call site.
Preview changes without modifying any files:
schemashift migrate ./src -f joi -t zod --dry-run
Run the full migration with verbose output:
schemashift migrate ./src -f joi -t zod --verbose
Generate a unified diff for code review:
schemashift migrate ./src -f joi -t zod --dry-run --output-diff changes.patch
Migrate with a Git branch for easy rollback:
schemashift migrate ./src -f joi -t zod --git-branch migrate/joi-to-zod
SchemaShift creates automatic backups before modifying files. Use
schemashift rollback to restore originals at any time.
After running the automated migration, review these items:
TODO(schemashift) comments and resolve each oneJoi.validate(data, schema) calls with schema.parse(data) or schema.safeParse(data)ZodError instead of returning { error, value }joi package from package.json once all schemas are convertedz.infer<typeof schema> type exports where you previously maintained manual interfacesjoiResolver to zodResolver in React Hook Form)Joi.ref() cross-references with .refine() callbacks.external() async validators and switch to .parseAsync()
Yes. SchemaShift provides an AST-based CLI that automatically converts Joi schemas
to Zod, including import rewriting, method chain transformation, and type helper
migration. Run schemashift migrate ./src -f joi -t zod to convert
an entire directory. Unsupported patterns receive inline TODO comments with
actionable guidance.
Joi's .when() conditional validation, .with() /
.without() field dependencies, .and() /
.nand() / .or() / .xor() group constraints,
and Joi.ref() cross-field references have no direct Zod equivalents.
These are migrated to .superRefine() or .refine()
patterns in Zod, with SchemaShift generating scaffolding code and TODO comments
for manual review.
Zod is TypeScript-first and provides automatic type inference via
z.infer, eliminating the need to maintain separate TypeScript
interfaces. Zod has a smaller bundle size, faster validation performance, and
a growing ecosystem with first-class support in tRPC, React Hook Form, and
Next.js. Joi was designed for JavaScript and lacks native TypeScript type inference.
Convert your Joi schemas to Zod in minutes. Free to use.
npm install -g schemashift-cli