Claude Code Plugins

Community-maintained marketplace

Feedback

migrating-v3-to-v4

@djankies/claude-configs
0
0

Complete migration guide from Zod v3 to v4 covering all breaking changes and upgrade patterns

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 error parameter
  • 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