Migrate Valibot to Zod

Convert Valibot’s functional pipe-based API to Zod’s fluent method chains. Whether you need broader ecosystem support, team familiarity with Zod, or first-class tRPC/Drizzle integration, SchemaShift automates the transformation.

Quick Start

Install SchemaShift globally and run the migration:

npm install -g schemashift-cli
schemashift migrate ./src -f valibot -t zod
Pro+ Required

Valibot → Zod migration requires a Pro or Team license. View pricing.

Before & After

Before (Valibot)
import * as v from 'valibot';

const UserSchema = v.object({
  name: v.pipe(v.string(), v.minLength(2), v.maxLength(100)),
  email: v.pipe(v.string(), v.email()),
  age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
  role: v.picklist(['admin', 'user', 'guest']),
  tags: v.array(v.string()),
  bio: v.nullable(v.string()),
  score: v.pipe(
    v.number(),
    v.transform((n) => Math.round(n))
  ),
});

type User = v.InferOutput<typeof UserSchema>;
After (Zod)
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
  role: z.enum(['admin', 'user', 'guest']),
  tags: z.array(z.string()),
  bio: z.string().nullable(),
  score: z.number().transform((n) => Math.round(n)),
});

type User = z.infer<typeof UserSchema>;

Conversion Reference

Valibot Zod Notes
v.object({...}) z.object({...}) Direct mapping
v.string() z.string() Direct mapping
v.number() z.number() Direct mapping
v.boolean() z.boolean() Direct mapping
v.pipe(v.string(), v.minLength(3)) z.string().min(3) Pipe unwrapped; actions become chained methods
v.pipe(v.string(), v.email()) z.string().email() Pipe unwrapped; built-in validators chained
v.optional(schema) schema.optional() Wrapper → chained method
v.nullable(schema) schema.nullable() Wrapper → chained method
v.picklist(['a', 'b']) z.enum(['a', 'b']) Direct mapping
v.union([a, b]) z.union([a, b]) Direct mapping
v.check(fn, msg) .refine(fn, msg) Custom validation; argument order matches
v.transform(fn) .transform(fn) Inside pipe → chained method
v.array(v.string()) z.array(z.string()) Direct mapping
v.InferOutput<typeof S> z.infer<typeof S> Type helper rewritten automatically
v.InferInput<typeof S> z.input<typeof S> Input type helper rewritten

Edge Cases & Gotchas

Automated Migration

Run the full migration with dry-run first to preview changes:

# Preview changes without modifying files
schemashift migrate ./src -f valibot -t zod --dry-run

# Run the actual migration
schemashift migrate ./src -f valibot -t zod

# Verbose with report
schemashift migrate ./src -f valibot -t zod -v --report html

# Canary migration: convert 20% of files first
schemashift migrate ./src -f valibot -t zod --canary 20

Post-Migration Checklist

Frequently Asked Questions

How do I convert Valibot pipe() calls to Zod?

Valibot’s v.pipe(v.string(), v.minLength(3), v.email()) becomes z.string().min(3).email() in Zod. SchemaShift automatically unwraps pipe() calls, extracts the base type as the first argument, and chains the remaining validation actions as Zod method calls.

Why would I migrate from Valibot back to Zod?

Common reasons include ecosystem compatibility (tRPC, Drizzle, and many libraries have first-class Zod support), team familiarity with Zod’s fluent API, needing discriminatedUnion or branded types, or consolidating on a single schema library across a large codebase.

Does SchemaShift handle Valibot’s wrapper functions like v.optional()?

Yes. Valibot uses wrapper functions like v.optional(v.string()) and v.nullable(v.string()), while Zod uses chained methods like z.string().optional() and z.string().nullable(). SchemaShift automatically converts the wrapper-style syntax to Zod’s chain-style syntax.

Related Guides

Ready to migrate?

Convert your Valibot schemas to Zod automatically with SchemaShift.

Get SchemaShift