| name | migrating-v3-to-v4 |
| description | Complete migration guide from Zod v3 to v4 covering all breaking changes and upgrade patterns |
Migrating to Zod v4
Purpose
Comprehensive guide for migrating existing Zod v3 codebases to v4, covering all breaking changes, migration patterns, and testing strategies.
Migration Overview
Zod v4 introduced major performance improvements and API refinements:
- 100x reduction in TypeScript instantiations
- 14x faster string parsing
- 57% smaller bundle size
- Simplified API surface with consistent patterns
Breaking changes are intentional improvements that require code updates.
Breaking Changes
1. String Format Methods → Top-Level Functions
Impact: Affects ~90% of Zod users using email/uuid/url validation
Before (v3):
const emailSchema = z.string().email();
const uuidSchema = z.string().uuid();
const datetimeSchema = z.string().datetime();
const urlSchema = z.string().url();
const ipSchema = z.string().ipv4();
const jwtSchema = z.string().jwt();
After (v4):
const emailSchema = z.email();
const uuidSchema = z.uuid();
const datetimeSchema = z.iso.datetime();
const urlSchema = z.url();
const ipSchema = z.ipv4();
const jwtSchema = z.jwt();
Migration script:
find ./src -name "*.ts" -o -name "*.tsx" | xargs sed -i '' \
-e 's/z\.string()\.email()/z.email()/g' \
-e 's/z\.string()\.uuid()/z.uuid()/g' \
-e 's/z\.string()\.datetime()/z.iso.datetime()/g' \
-e 's/z\.string()\.url()/z.url()/g' \
-e 's/z\.string()\.ipv4()/z.ipv4()/g' \
-e 's/z\.string()\.ipv6()/z.ipv6()/g' \
-e 's/z\.string()\.jwt()/z.jwt()/g' \
-e 's/z\.string()\.base64()/z.base64()/g'
2. Error Customization → Unified error Parameter
Impact: Affects error handling and user-facing validation messages
Before (v3):
z.string({ message: "Required field" });
z.string({ invalid_type_error: "Must be a string" });
z.string({ required_error: "This field is required" });
z.object({}, { errorMap: customErrorMap });
After (v4):
z.string({ error: "Required field" });
z.string({ error: "Must be a string" });
z.string({ error: "This field is required" });
z.object({}, { error: customErrorMap });
Migration pattern:
All error-related parameters now use single error field that accepts strings or error map functions.
3. Schema Merge → Extend
Impact: Affects schema composition patterns
Before (v3):
const baseSchema = z.object({ id: z.string() });
const extendedSchema = baseSchema.merge(
z.object({ name: z.string() })
);
After (v4):
const baseSchema = z.object({ id: z.string() });
const extendedSchema = baseSchema.extend({
name: z.string()
});
Migration script:
find ./src -name "*.ts" | xargs sed -i '' \
-e 's/\.merge(z\.object(\([^)]*\)))/\.extend(\1)/g'
4. Refinements → New Architecture
Impact: Custom validation logic and error messages
Before (v3):
z.string().refine((val) => val.length > 5, {
message: "Too short"
});
After (v4):
z.string().refine((val) => val.length > 5, {
error: "Too short"
});
Error customization in refinements also uses unified error parameter.
5. String Transformations (New in v4)
Not a breaking change, but highly recommended:
Before (v3 pattern):
const schema = z.string();
const result = schema.parse(input.trim().toLowerCase());
After (v4 recommended):
const schema = z.string().trim().toLowerCase();
const result = schema.parse(input);
Benefits:
- Declarative transformation pipeline
- Type-safe and composable
- Better error messages
- Automatic type inference
Migration Process
Step 1: Upgrade Package
npm install zod@^4.0.0
Or with specific version:
npm install zod@4.0.0
Step 2: Run Compatibility Check
Use validation skill to identify deprecated patterns:
/review zod-compatibility
Or manually scan:
grep -r "z\.string()\.email(" ./src
grep -r "z\.string()\.uuid(" ./src
grep -r "\.merge(" ./src
grep -r "message:" ./src | grep -v "error:"
Step 3: Apply Automated Migrations
Run migration scripts:
./migrate-string-formats.sh
./migrate-error-params.sh
./migrate-merge-to-extend.sh
Step 4: Handle Manual Migrations
Some patterns require manual review:
Complex error maps:
const customErrorMap: ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
return { message: "Invalid type!" };
}
return { message: ctx.defaultError };
};
z.string({ errorMap: customErrorMap });
Migration:
const customErrorMap: ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
return { message: "Invalid type!" };
}
return { message: ctx.defaultError };
};
z.string({ error: customErrorMap });
Nested schema merges:
const a = z.object({ x: z.string() });
const b = z.object({ y: z.number() });
const c = a.merge(b);
Migration:
const a = z.object({ x: z.string() });
const b = z.object({ y: z.number() });
const c = a.extend({ y: z.number() });
Step 5: Add String Transformations
Identify manual string operations and migrate to built-in methods:
Before:
const emailSchema = z.email();
const processEmail = (input: string) => {
const trimmed = input.trim().toLowerCase();
return emailSchema.parse(trimmed);
};
After:
const emailSchema = z.email().trim().toLowerCase();
const processEmail = (input: string) => {
return emailSchema.parse(input);
};
Step 6: Run Tests
Comprehensive test suite after migration:
npm test
Check for:
- Schema validation logic still works
- Error messages display correctly
- Type inference remains correct
- No runtime errors from API changes
Step 7: Update Documentation
Update code comments and docs referencing Zod APIs:
- Remove references to deprecated methods
- Update examples to v4 patterns
- Document new string transformation methods
Common Migration Issues
Issue 1: Type Errors After String Format Migration
Problem:
const emailSchema = z.string().email();
type Email = z.infer<typeof emailSchema>;
After migration:
const emailSchema = z.email();
type Email = z.infer<typeof emailSchema>;
Solution: Type inference still works, but type is now more specific to email strings.
Issue 2: Custom Error Maps Not Working
Problem: Error map using old parameter names
Solution: Update error map to use unified error parameter and ensure function signature matches ZodErrorMap type.
Issue 3: Merge Breaking Complex Compositions
Problem: Nested merges don't translate directly to extend
Solution: Use multiple extend calls or restructure schema:
const result = base.extend(ext1.shape).extend(ext2.shape);
Issue 4: Tests Fail with Different Error Messages
Problem: v4 error messages may differ from v3
Solution: Update test assertions to match new error format or use error codes instead of messages:
expect(result.error.issues[0].code).toBe(z.ZodIssueCode.invalid_type);
Testing Strategy
Unit Tests
Test schema validation logic:
import { z } from 'zod';
describe('User schema validation', () => {
const userSchema = z.object({
email: z.email().trim().toLowerCase(),
username: z.string().trim().min(3)
});
it('validates correct user data', () => {
const result = userSchema.safeParse({
email: ' USER@EXAMPLE.COM ',
username: ' john '
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('user@example.com');
expect(result.data.username).toBe('john');
}
});
it('rejects invalid email', () => {
const result = userSchema.safeParse({
email: 'not-an-email',
username: 'john'
});
expect(result.success).toBe(false);
});
});
Integration Tests
Test form validation with transformed data:
const formSchema = z.object({
email: z.email().trim().toLowerCase(),
password: z.string().min(8)
});
const handleSubmit = async (formData: FormData) => {
const result = formSchema.safeParse({
email: formData.get('email'),
password: formData.get('password')
});
if (!result.success) {
return { errors: result.error.flatten() };
}
await createUser(result.data);
};
Type Tests
Verify type inference works correctly:
const schema = z.email().trim();
type Email = z.infer<typeof schema>;
const email: Email = 'test@example.com';
Migration Checklist
- Upgrade Zod package to v4
- Run compatibility validation
- Migrate string format methods to top-level functions
- Update error customization to use
errorparameter - Replace
.merge()with.extend() - Add string transformations where applicable
- Update error maps and refinements
- Run full test suite
- Update documentation and examples
- Review type inference correctness
- Test error handling in production-like scenarios
- Update CI/CD pipelines if needed
Performance Gains
After migration, expect:
- Faster TypeScript compilation - 100x reduction in type instantiations
- Faster runtime parsing - 14x improvement for string validation
- Smaller bundle size - 57% reduction
- Better error messages - Clearer validation feedback
Monitor performance improvements:
npm run build -- --stats
Compare bundle size before/after migration.
References
- Validation skill: Use the validating-schema-basics skill from the zod-4 plugin
- v4 Features: Use the validating-string-formats skill from the zod-4 plugin
- Error handling: Use the customizing-errors skill from the zod-4 plugin
Success Criteria
- ✅ All v3 deprecated APIs replaced with v4 equivalents
- ✅ Tests pass with 100% success rate
- ✅ No TypeScript compilation errors
- ✅ Error messages display correctly in UI
- ✅ Type inference works as expected
- ✅ Performance improvements measurable
- ✅ Documentation updated to reflect v4 patterns