Migrate io-ts to Effect Schema

A complete guide to converting io-ts codecs to Effect Schema. Stay in the functional programming ecosystem while modernizing your validation layer. Convert t.type to S.Struct, replace fp-ts pipe with Effect pipe, and gain access to the full Effect ecosystem.

Quick Start

Convert your io-ts codecs to Effect Schema:

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

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

Before & After

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

const DatabaseConfig = t.type({
  host: t.string,
  port: t.number,
  database: t.string,
  ssl: t.boolean,
});

const AppConfig = t.type({
  db: DatabaseConfig,
  env: t.union([
    t.literal('development'),
    t.literal('staging'),
    t.literal('production'),
  ]),
  features: t.array(t.string),
  limits: t.partial({
    maxConnections: t.number,
    timeout: t.number,
  }),
});

type AppConfig = t.TypeOf<typeof AppConfig>;

const ServerConfig = t.intersection([
  AppConfig,
  t.type({ port: t.number }),
]);

// Decoding
const parseConfig = (raw: unknown): AppConfig =>
  pipe(
    AppConfig.decode(raw),
    fold(
      (errors) => { throw new Error(String(errors)); },
      (config) => config
    )
  );
AFTER — Effect Schema
// schemas/config.ts
import * as S from '@effect/schema/Schema';
import { pipe } from 'effect/Function';

const DatabaseConfig = S.Struct({
  host: S.String,
  port: S.Number,
  database: S.String,
  ssl: S.Boolean,
});

const AppConfig = S.Struct({
  db: DatabaseConfig,
  env: S.Union(
    S.Literal('development'),
    S.Literal('staging'),
    S.Literal('production'),
  ),
  features: S.Array(S.String),
  limits: S.partial(S.Struct({
    maxConnections: S.Number,
    timeout: S.Number,
  })),
});

type AppConfig = S.Schema.Type<typeof AppConfig>;

const ServerConfig = pipe(
  AppConfig,
  S.extend(S.Struct({ port: S.Number })),
);

// Decoding
const parseConfig = (raw: unknown): AppConfig =>
  S.decodeUnknownSync(AppConfig)(raw);

Conversion Reference

io-ts Effect Schema Notes
t.string S.String Capitalized, no parentheses
t.number S.Number
t.boolean S.Boolean
t.type({ ... }) S.Struct({ ... })
t.partial({ ... }) S.partial(S.Struct({ ... })) Wrapping function
t.array(X) S.Array(X)
t.union([A, B]) S.Union(A, B) Spread args, not array
t.intersection([A, B]) pipe(A, S.extend(B)) Uses Effect pipe
t.literal('x') S.Literal('x')
t.null S.Null
t.undefined S.Undefined
t.void S.Void
t.unknown S.Unknown
t.record(t.string, X) S.Record({ key: S.String, value: X })
t.tuple([A, B]) S.Tuple(A, B) Spread args, not array
t.keyof({ a: null }) S.Literal('a') Or S.Union(S.Literal('a'), ...)
t.TypeOf<typeof X> S.Schema.Type<typeof X>
t.OutputOf<typeof X> S.Schema.Type<typeof X>
pipe (fp-ts) pipe (effect/Function) Same concept, different import
codec.decode(input) S.decodeUnknownSync(schema)(input) Throws on failure
codec.encode(value) S.encodeSync(schema)(value) Encode is preserved

Edge Cases & Gotchas

Automated Migration with SchemaShift

# Preview changes with dry run
npx schemashift-cli migrate ./src --from io-ts --to effect --dry-run

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

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

# Analyze behavioral differences first
npx schemashift-cli analyze ./src --behavioral io-ts:effect

Manual Migration Checklist

Frequently Asked Questions

Why migrate from io-ts to Effect Schema instead of Zod?

If your codebase already uses fp-ts patterns and functional programming, Effect Schema provides a natural upgrade path. It retains the functional paradigm with pipe-based composition, supports encode/decode (not just parse), and integrates with the broader Effect ecosystem for error handling, concurrency, and dependency injection. Choose Zod if you prefer a simpler, more mainstream API.

How does Effect Schema handle union types compared to io-ts?

io-ts uses t.union([A, B]) with array syntax. Effect Schema uses S.Union() with spread arguments: S.Union(A, B) instead of S.Union([A, B]). This is a subtle but important difference. For intersections, io-ts t.intersection([A, B]) becomes pipe(A, S.extend(B)) using Effect's pipe function.

What changes for decode and encode operations?

io-ts uses codec.decode(input) returning Either. Effect Schema uses S.decodeUnknownEither(schema)(input) or S.decodeUnknownSync(schema)(input) for synchronous parsing that throws. The encode direction uses S.encodeSync(schema)(value). Effect Schema preserves bidirectional encoding that Zod does not support.

Related Guides

Ready to adopt Effect?

SchemaShift automates io-ts to Effect Schema migration with codec conversion, fp-ts pipe rewriting, and inline guidance for complex patterns.

Get SchemaShift