Migrate io-ts to Zod

A comprehensive guide to converting io-ts codecs to Zod schemas. Learn how to replace t.type, t.string, and t.TypeOf with their Zod equivalents, handle fp-ts Either patterns, and remove functional programming boilerplate.

Quick Start

Migrate your entire project with a single command:

npx schemashift-cli migrate ./src --from io-ts --to zod
Pro Feature

io-ts to Zod migration requires a Pro or Team license. View pricing.

Before & After

BEFORE — io-ts
// schemas/user.ts
import * as t from 'io-ts';
import { pipe } from 'fp-ts/function';
import { fold } from 'fp-ts/Either';

const UserCodec = t.type({
  id: t.number,
  name: t.string,
  email: t.string,
  role: t.union([
    t.literal('admin'),
    t.literal('user'),
    t.literal('guest')
  ]),
  tags: t.array(t.string),
  address: t.partial({
    street: t.string,
    city: t.string,
    zip: t.string,
  }),
});

type User = t.TypeOf<typeof UserCodec>;

// Decoding with Either
const result = pipe(
  UserCodec.decode(input),
  fold(
    (errors) => { throw new Error('Invalid'); },
    (user) => user
  )
);
AFTER — Zod
// schemas/user.ts
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
  role: z.union([
    z.literal('admin'),
    z.literal('user'),
    z.literal('guest')
  ]),
  tags: z.array(z.string()),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string(),
  }).partial(),
});

type User = z.infer<typeof UserSchema>;

// Parsing with safeParse
const result = UserSchema.safeParse(input);
if (!result.success) {
  throw new Error('Invalid');
}
const user = result.data;

Conversion Reference

io-ts Zod Notes
t.string z.string() Note: Zod uses function calls
t.number z.number()
t.boolean z.boolean()
t.type({ ... }) z.object({ ... }) All properties required
t.partial({ ... }) z.object({ ... }).partial() All properties optional
t.array(X) z.array(X)
t.union([A, B]) z.union([A, B])
t.intersection([A, B]) A.merge(B) For object types; use .and() for others
t.literal('x') z.literal('x')
t.null z.null()
t.undefined z.undefined()
t.void z.void()
t.unknown z.unknown()
t.record(t.string, X) z.record(z.string(), X)
t.tuple([A, B]) z.tuple([A, B])
t.keyof({ a: null, b: null }) z.enum(['a', 'b'])
t.TypeOf<typeof X> z.infer<typeof X> Type extraction
t.OutputOf<typeof X> z.infer<typeof X> Zod has no separate output type for basic schemas
t.Int z.number().int() Branded integer becomes a refinement

Edge Cases & Gotchas

Automated Migration with SchemaShift

# Dry run first to preview changes
npx schemashift-cli migrate ./src --from io-ts --to zod --dry-run

# Run the migration
npx schemashift-cli migrate ./src --from io-ts --to zod

# With verbose output and HTML report
npx schemashift-cli migrate ./src --from io-ts --to zod --verbose --report html

# Export a unified diff for review
npx schemashift-cli migrate ./src --from io-ts --to zod --dry-run --output-diff changes.patch

Manual Migration Checklist

Frequently Asked Questions

How do I handle io-ts Either monads when migrating to Zod?

io-ts decode() returns an fp-ts Either (Left for errors, Right for success). Zod uses safeParse() which returns { success: boolean, data?, error? }. Replace pipe(codec.decode(input), fold(...)) with const result = schema.safeParse(input) and check result.success.

Can I migrate io-ts branded types to Zod?

Yes. io-ts branded types like t.brand(t.number, (n): n is Positive => n > 0, 'Positive') map to Zod's z.number().refine(n => n > 0).brand('Positive'). SchemaShift auto-converts simple branded types and adds TODO comments for complex custom brands that need manual review.

What happens to fp-ts pipe imports during migration?

SchemaShift removes fp-ts pipe imports that are only used for io-ts codec decoding. If pipe is used elsewhere in your code for other fp-ts operations, it is preserved. The tool analyzes usage context to determine which imports can be safely removed.

Related Guides

Ready to migrate?

SchemaShift automates io-ts to Zod migration with AST-based transforms, fp-ts cleanup, and inline TODO guidance.

Get SchemaShift