A comprehensive guide to converting Zod schemas to ArkType's string-based type system.
Learn how to replace z.object({}) with type({}), convert method chains
to concise string syntax like "string.email", and leverage ArkType's superior
TypeScript inference.
Convert your Zod schemas to ArkType with one command:
npx schemashift-cli migrate ./src --from zod --to arktype
Zod to ArkType migration requires a Pro or Team license. View pricing.
// schemas/user.ts
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive(),
role: z.enum(['admin', 'user', 'guest']),
isActive: z.boolean(),
tags: z.array(z.string()),
score: z.number().optional(),
});
type User = z.infer<typeof UserSchema>;
const ResponseSchema = z.union([
z.object({ status: z.literal('ok'), data: UserSchema }),
z.object({ status: z.literal('error'), message: z.string() }),
]);
const validated = UserSchema.parse(input);
const safe = UserSchema.safeParse(input);
// schemas/user.ts
import { type } from 'arktype';
const UserSchema = type({
id: 'string.uuid',
name: '1 <= string <= 100',
email: 'string.email',
age: 'number.integer > 0',
role: "'admin' | 'user' | 'guest'",
isActive: 'boolean',
tags: 'string[]',
'score?': 'number',
});
type User = typeof UserSchema.infer;
const ResponseSchema = type(
{ status: "'ok'", data: UserSchema },
'|',
{ status: "'error'", message: 'string' }
);
const validated = UserSchema.assert(input);
const safe = UserSchema(input);
| Zod | ArkType | Notes |
|---|---|---|
z.string() |
'string' |
String literal syntax |
z.number() |
'number' |
|
z.boolean() |
'boolean' |
|
z.object({ ... }) |
type({ ... }) |
|
z.string().email() |
'string.email' |
Built-in string subtypes |
z.string().uuid() |
'string.uuid' |
|
z.string().url() |
'string.url' |
|
z.string().min(N).max(M) |
'N <= string <= M' |
Range expression syntax |
z.number().int() |
'number.integer' |
|
z.number().positive() |
'number > 0' |
Comparison expression |
z.enum(['a', 'b']) |
"'a' | 'b'" |
Union of literals as string |
z.literal('x') |
"'x'" |
Quoted string literal |
z.array(z.string()) |
'string[]' |
Array shorthand |
.optional() |
'type?' (in object key) |
Append ? to the key name |
z.union([A, B]) |
type(A, '|', B) |
Infix union syntax |
z.infer<typeof X> |
typeof X.infer |
Property access, not generic |
.refine(fn) |
.narrow(fn) |
|
.transform(fn) |
.pipe(fn) |
|
z.record(K, V) |
type('Record<string, V>') |
Uses TypeScript syntax |
schema.parse(input) |
schema.assert(input) |
Throws on failure |
schema.safeParse(input) |
schema(input) |
Returns data or ArkErrors |
'string.email' and 'number > 0' instead
of method chains. This is fundamentally different from Zod and requires understanding ArkType's
domain-specific language. TypeScript still provides full type inference from these strings.
z.lazy() for recursive types. ArkType requires scope() to define
mutually recursive types: const types = scope({ tree: { value: 'string', children: 'tree[]' } }).export().
This is more powerful but has different ergonomics.
.brand() for nominal types. If your codebase relies on
branded types for type safety, you will need to use TypeScript's manual branding pattern
(intersection with { readonly __brand: unique symbol }). SchemaShift adds TODO comments.
.superRefine() with ctx.addIssue() does not have a direct equivalent.
Simple cases map to .narrow(), but multi-issue validation requires ArkType's custom
error handling patterns.
z.string().optional() makes the value optional. In ArkType, you mark the key
itself: type({ 'name?': 'string' }). This means optional is declared in the object
definition, not on the type.
# Dry run to see what changes
npx schemashift-cli migrate ./src --from zod --to arktype --dry-run
# Run the migration
npx schemashift-cli migrate ./src --from zod --to arktype
# With verbose output
npx schemashift-cli migrate ./src --from zod --to arktype --verbose --report html
# Check compatibility first
npx schemashift-cli compat ./src --from zod --to arktype
import { z } from 'zod' with import { type } from 'arktype'z.object({}) to type({})z.string() → 'string'.email() → 'string.email'z.enum(['a','b']) → "'a' | 'b'"key: z.string().optional() → 'key?': 'string'z.infer<typeof X> with typeof X.infer.refine() to .narrow().parse() to .assert().safeParse() to direct call schema(input)scope().brand() and .superRefine() usagepackage.json: add arktype, remove zodnpx tsc --noEmit to verify type safety
ArkType uses string literals to define types instead of method chains. For example,
z.string().email() in Zod becomes simply 'string.email' in ArkType.
Object schemas use type({ name: 'string', age: 'number' }) instead of
z.object({ name: z.string(), age: z.number() }). This provides more concise
syntax with full TypeScript type inference.
Zod's .refine() maps to ArkType's .narrow() method. For example,
z.number().refine(n => n > 0) becomes type('number').narrow(n => n > 0).
For more complex validation with type narrowing, ArkType's .narrow() provides stronger
TypeScript inference than Zod's refine.
ArkType does not have a direct equivalent to Zod's .brand(). If you rely on branded types
for nominal typing, you will need to use TypeScript's built-in branded type pattern (intersection
with a unique symbol) or restructure your type system. SchemaShift adds TODO comments for branded
type usage that needs manual attention.
SchemaShift converts Zod schemas to ArkType's string syntax automatically, handling type expressions, optional keys, and import rewriting.
Get SchemaShift