Migrate Zod schemas back to Yup for legacy codebase integration, team familiarity with Yup’s API, or ecosystem requirements where Yup is the established standard. SchemaShift handles the AST-based transformation automatically.
Install SchemaShift globally and run the migration with a single command:
npm install -g schemashift-cli
schemashift migrate ./src -f zod -t yup
Backward migrations (Zod → Yup) require a Pro or Team license. View pricing.
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(0).optional(),
role: z.enum(['admin', 'user', 'guest']),
status: z.literal('active'),
bio: z.string().nullable(),
});
type User = z.infer<typeof UserSchema>;
import * as yup from 'yup';
const UserSchema = yup.object({
name: yup.string().required().min(2).max(100),
email: yup.string().required().email(),
age: yup.number().integer().min(0).notRequired(),
role: yup.mixed().oneOf(['admin', 'user', 'guest']).required(),
status: yup.mixed().oneOf(['active']).required(),
bio: yup.string().nullable().required(),
});
type User = yup.InferType<typeof UserSchema>;
| Zod | Yup | Notes |
|---|---|---|
z.object({...}) |
yup.object({...}) |
Direct mapping |
z.string() |
yup.string().required() |
Zod is required by default; Yup needs explicit .required() |
z.number() |
yup.number().required() |
Same required semantics difference |
z.boolean() |
yup.boolean().required() |
Same required semantics difference |
.optional() |
.notRequired() |
Yup uses .notRequired() instead of .optional() |
.nullable() |
.nullable() |
Direct mapping |
.refine(fn, msg) |
.test(name, msg, fn) |
Different argument order; Yup requires a test name |
z.enum(['a', 'b']) |
yup.mixed().oneOf(['a', 'b']) |
Yup has no native enum; uses .oneOf() |
z.literal('val') |
yup.mixed().oneOf(['val']) |
Expressed as single-value .oneOf() |
z.union([a, b]) |
yup.mixed().oneOf([...]) |
Limited; works for literal unions only |
z.array(z.string()) |
yup.array().of(yup.string()) |
Direct mapping with .of() |
z.infer<typeof S> |
yup.InferType<typeof S> |
Type helper rewritten automatically |
z.record(z.string(), z.number()) allows arbitrary keys with typed values.
Yup has no direct record equivalent. SchemaShift converts to yup.object() with a
TODO comment noting the limitation. You may need yup.lazy() for dynamic keys.
z.tuple([z.string(), z.number()])).
Yup arrays cannot enforce different types per index. The migration converts to
yup.array() with a TODO noting the lost type granularity.
/* TODO(schemashift): ... */ comment. Use yup.lazy() with a
switch on the discriminator field as the manual replacement.
.superRefine() receives a context with ctx.addIssue().
Yup’s .test() returns true/false or
this.createError(). Complex refinements need manual adjustment of error reporting logic.
yup.number() will accept
"42" and coerce it to 42. Zod does not coerce unless you use
z.coerce. After migration, your schemas may accept inputs that Zod previously rejected.
Add .strict() in Yup to disable coercion if needed.
Run the full migration with dry-run first to preview changes:
# Preview changes without modifying files
schemashift migrate ./src -f zod -t yup --dry-run
# Run the actual migration
schemashift migrate ./src -f zod -t yup
# Export a diff for code review
schemashift migrate ./src -f zod -t yup --dry-run --output-diff changes.patch
# Verbose output with HTML report
schemashift migrate ./src -f zod -t yup -v --report html
npm install yup if not already installedTODO(schemashift) comments and resolve manuallyzodResolver → yupResolver.refine() → .test() conversions return correct error messagesz.discriminatedUnion TODO comments and implement yup.lazy() replacementsnpx tsc --noEmit to verify TypeScript compilation.strict() to Yup schemas if coercion is unwanted
Yes. SchemaShift supports backward migration from Zod to Yup using AST-based transformations.
Run schemashift migrate ./src -f zod -t yup to convert z.object,
z.string, z.enum, .refine, and other Zod patterns to
their Yup equivalents. This requires a Pro or Team license.
z.discriminatedUnion requires manual refactoring since Yup has no direct equivalent.
z.record maps to yup.object with limited key typing.
z.tuple converts to yup.array but loses per-element type safety.
z.branded types have no Yup equivalent. These patterns receive TODO comments with guidance.
Yes. Yup auto-coerces by default (e.g., yup.number() will coerce "42"
to 42), while Zod requires explicit z.coerce.number(). After migrating
from Zod to Yup, your schemas may accept inputs that were previously rejected. SchemaShift warns
about this behavioral difference during migration.