Convert Zod to Valibot

A complete guide to converting Zod schemas to Valibot. Understand the paradigm shift from method chaining to pipe()-based composition, reduce your bundle size, and migrate z.infer to v.InferOutput.

Quick Start

Convert your Zod schemas to Valibot automatically:

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

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

Before & After

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

const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(200),
  price: z.number().positive(),
  email: z.string().email(),
  status: z.enum(['active', 'draft', 'archived']),
  tags: z.array(z.string()).optional(),
  metadata: z.record(z.string(), z.unknown()),
});

type Product = z.infer<typeof ProductSchema>;

const CategorySchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('physical'), weight: z.number() }),
  z.object({ type: z.literal('digital'), url: z.string().url() }),
]);

const ValidProduct = ProductSchema.refine(
  (p) => p.price > 0,
  { message: 'Price must be positive' }
);
AFTER — Valibot
// schemas/product.ts
import * as v from 'valibot';

const ProductSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
  price: v.pipe(v.number(), v.minValue(0, 'Must be positive')),
  email: v.pipe(v.string(), v.email()),
  status: v.picklist(['active', 'draft', 'archived']),
  tags: v.optional(v.array(v.string())),
  metadata: v.record(v.string(), v.unknown()),
});

type Product = v.InferOutput<typeof ProductSchema>;

const CategorySchema = v.variant('type', [
  v.object({ type: v.literal('physical'), weight: v.number() }),
  v.object({ type: v.literal('digital'), url: v.pipe(v.string(), v.url()) }),
]);

const ValidProduct = v.pipe(
  ProductSchema,
  v.check((p) => p.price > 0, 'Price must be positive')
);

Conversion Reference

Zod Valibot Notes
z.string() v.string()
z.number() v.number()
z.boolean() v.boolean()
z.object({ ... }) v.object({ ... })
z.array(X) v.array(X)
z.string().email() v.pipe(v.string(), v.email()) Validations wrap in pipe()
z.string().min(N) v.pipe(v.string(), v.minLength(N))
z.string().max(N) v.pipe(v.string(), v.maxLength(N))
z.number().min(N) v.pipe(v.number(), v.minValue(N))
z.number().int() v.pipe(v.number(), v.integer())
.optional() v.optional(schema) Wrapping function, not method
.nullable() v.nullable(schema) Wrapping function, not method
z.enum(['a', 'b']) v.picklist(['a', 'b'])
z.union([A, B]) v.union([A, B])
z.discriminatedUnion('k', [...]) v.variant('k', [...])
z.literal('x') v.literal('x')
z.record(K, V) v.record(K, V)
z.tuple([A, B]) v.tuple([A, B])
.refine(fn, msg) v.check(fn, msg) Used inside v.pipe()
.transform(fn) v.transform(fn) Used inside v.pipe()
z.infer<typeof X> v.InferOutput<typeof X> Type inference helper

Edge Cases & Gotchas

Automated Migration with SchemaShift

# Preview changes before migrating
npx schemashift-cli migrate ./src --from zod --to valibot --dry-run

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

# Estimate bundle size impact
npx schemashift-cli analyze ./src --bundle zod:valibot

# Migrate with cross-file resolution
npx schemashift-cli migrate ./src --from zod --to valibot --cross-file

Manual Migration Checklist

Frequently Asked Questions

Why migrate from Zod to Valibot?

Valibot offers significantly smaller bundle sizes through its tree-shakeable functional design. While Zod is around 13kB minified+gzipped, Valibot schemas only include the functions you actually use, often resulting in under 1kB for typical schemas. This makes it ideal for frontend applications where bundle size matters.

What is Valibot's pipe() and how does it differ from Zod chaining?

Zod uses method chaining: z.string().email().min(1). Valibot uses a pipe() function that wraps the base type and validations: v.pipe(v.string(), v.email(), v.minLength(1)). This functional approach enables better tree-shaking since unused validation functions are not bundled.

Does Valibot support discriminatedUnion like Zod?

Yes, Valibot has v.variant() which serves the same purpose as z.discriminatedUnion(). The syntax differs: z.discriminatedUnion('type', [...]) becomes v.variant('type', [...]). SchemaShift handles this conversion automatically.

Related Guides

Ready to shrink your bundle?

SchemaShift automates Zod to Valibot conversion with pipe() wrapping, import rewriting, and resolver migration.

Get SchemaShift