Migrate Zod to Superstruct

A step-by-step guide to converting Zod schemas to Superstruct's bare function API. Learn how to replace namespaced z.* calls with direct function imports like object(), string(), and optional(), and migrate z.infer to Infer.

Quick Start

Convert your Zod schemas to Superstruct automatically:

npx schemashift-cli migrate ./src --from zod --to superstruct
Pro+ Feature

Zod to Superstruct migration requires a Pro or Team license. View pricing.

Before & After

BEFORE — Zod
// schemas/order.ts
import { z } from 'zod';

const OrderSchema = z.object({
  id: z.string(),
  amount: z.number().int().positive(),
  currency: z.enum(['USD', 'EUR', 'GBP']),
  status: z.enum(['pending', 'paid', 'shipped']),
  items: z.array(z.object({
    name: z.string(),
    quantity: z.number().int(),
    price: z.number(),
  })),
  notes: z.string().optional(),
  discount: z.number().nullable(),
  createdAt: z.date(),
});

type Order = z.infer<typeof OrderSchema>;

const validated = OrderSchema.parse(input);

const PartialOrder = OrderSchema.partial();

const ItemSchema = OrderSchema.shape.items;
AFTER — Superstruct
// schemas/order.ts
import {
  object, string, number, integer, enums,
  array, optional, nullable, date, partial,
  type Infer,
} from 'superstruct';
import { refine } from 'superstruct';

const OrderSchema = object({
  id: string(),
  amount: refine(integer(), 'positive', (v) => v > 0),
  currency: enums(['USD', 'EUR', 'GBP']),
  status: enums(['pending', 'paid', 'shipped']),
  items: array(object({
    name: string(),
    quantity: integer(),
    price: number(),
  })),
  notes: optional(string()),
  discount: nullable(number()),
  createdAt: date(),
});

type Order = Infer<typeof OrderSchema>;

const validated = create(input, OrderSchema);

const PartialOrder = partial(OrderSchema);

/* TODO(schemashift): Superstruct does not support
   .shape access. Extract the items struct separately. */

Conversion Reference

Zod Superstruct Notes
z.string() string() Bare function, no namespace
z.number() number()
z.boolean() boolean()
z.object({ ... }) object({ ... })
z.array(X) array(X)
z.enum(['a', 'b']) enums(['a', 'b']) Note: enums not enum
z.literal('x') literal('x')
z.union([A, B]) union([A, B])
z.tuple([A, B]) tuple([A, B])
z.record(K, V) record(K, V)
.optional() optional(X) Wrapping function, not method
.nullable() nullable(X) Wrapping function, not method
z.number().int() integer() Dedicated integer struct
z.date() date()
z.any() any()
z.unknown() unknown()
.refine(fn, msg) refine(struct, name, fn) Wrapping function with name parameter
z.infer<typeof X> Infer<typeof X> Named import from superstruct
schema.parse(input) create(input, schema) Arguments are reversed
schema.partial() partial(schema) Wrapping function

Edge Cases & Gotchas

Automated Migration with SchemaShift

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

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

# With cross-file resolution
npx schemashift-cli migrate ./src --from zod --to superstruct --cross-file --verbose

# Generate a diff for code review
npx schemashift-cli migrate ./src --from zod --to superstruct --dry-run --output-diff changes.patch

Manual Migration Checklist

Frequently Asked Questions

Why would I migrate from Zod to Superstruct?

Superstruct is lightweight (~3.6kB), has zero dependencies, and uses a bare function API that some developers prefer for its simplicity. It is a good fit for projects that want minimal validation overhead and do not need Zod's extensive feature set like discriminatedUnion, branded types, or transform chains.

How does Superstruct's optional() differ from Zod's .optional()?

In Zod, optional is a method chained on a type: z.string().optional(). In Superstruct, optional is a wrapper function: optional(string()). This inversion means optional wraps the entire struct definition rather than being appended to it. Similarly, nullable() wraps the struct: nullable(string()).

Does Superstruct support discriminatedUnion?

No. Superstruct does not have a discriminatedUnion equivalent. You can use union() which tests each variant, but it lacks the performance optimization of discriminated unions that check a single key first. For complex union types, consider using refine() with custom logic or restructuring your data model.

Related Guides

Ready to simplify your schemas?

SchemaShift automates Zod to Superstruct conversion, handling bare function imports, optional wrapping, and refine patterns.

Get SchemaShift