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.
Convert your Zod schemas to Superstruct automatically:
npx schemashift-cli migrate ./src --from zod --to superstruct
Zod to Superstruct migration requires a Pro or Team license. View pricing.
// 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;
// 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. */
| 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 |
discriminatedUnion(). Use union() which tests
each variant sequentially. For performance-sensitive code with many variants, consider keeping a
manual discriminator check before validation.
.transform() maps to Superstruct's coerce(). The semantics differ:
coerce(string(), unknown(), (v) => String(v)) applies the coercion before validation,
while Zod's transform runs after. This can change behavior for invalid inputs.
.brand() for nominal typing,
you will need TypeScript's manual branding pattern or define() with a custom validator.
z.string(),
you import string directly: import { string, number, object } from 'superstruct'.
This can create naming conflicts with built-in JavaScript constructors.
schema.parse(input) as a method call. Superstruct uses
create(input, schema) as a standalone function with reversed argument order.
This affects every validation call site in your codebase.
Schema.shape.fieldName to access individual field schemas. Superstruct
does not expose this API. Extract field structs as separate variables instead.
# 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
import { z } from 'zod' with individual Superstruct importsz.object({}) to object({})z.string() / z.number() to bare string() / number()z.enum([]) to enums([]).optional() to optional(X) wrapper.nullable() to nullable(X) wrapperz.number().int() with integer().refine(fn) to refine(struct, name, fn).transform() with coerce()schema.parse(input) to create(input, schema)z.infer<typeof X> with Infer<typeof X>discriminatedUnion manually with union().shape accesses into standalone struct variablespackage.json: add superstruct, remove zodSuperstruct 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.
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()).
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.
SchemaShift automates Zod to Superstruct conversion, handling bare function imports, optional wrapping, and refine patterns.
Get SchemaShift