cheat sheet
valibot
Package-level reference for valibot — schemas, transforms, pipes, branding, safeParse, and the tree-shaking advantage over zod.
valibot
What it is
valibot is a schema-validation library for TypeScript that emphasises modular function-based composition rather than chained builder objects. Each schema and validator is an importable function — string, object, pipe, minLength, email — so bundlers tree-shake everything you don't use. The result is dramatically smaller production bundles compared with the chained-builder alternative (zod).
Reach for valibot when bundle size matters (edge functions, client-side widgets, mobile-targeted SPAs) and you want a zod-shaped API with proper tree-shaking. Reach for zod if you want the larger ecosystem (resolvers for every form library, generators, OpenAPI), or arktype for runtime-checked TypeScript-syntax schemas.
Install
npm install valibot
Output: added valibot to dependencies
pnpm add valibot
Output: added 1 package, linked from store
yarn add valibot
Output: added valibot
bun add valibot
Output: installed valibot
Versioning & Node support
Valibot reached 1.0 in mid-2025. The current line is valibot@1.x. Pre-1.0 versions exist in older lockfiles and had API churn.
- ESM-only library; works in Node 18+, all modern browsers, Deno, Bun, Cloudflare Workers, and Vercel Edge.
- TypeScript types bundled in the package.
- Semver is observed strictly post-1.0.
- No CJS distribution (intentional —
valibottargets modern toolchains).
Package metadata
- Maintainer: Fabian Hiller (creator) + community
- Project home: github.com/fabian-hiller/valibot
- Docs: valibot.dev
- npm: npmjs.com/package/valibot
- License: MIT
- First released: 2023
- Downloads: millions weekly — rapid growth since 1.0
Peer dependencies & extras
valibot has no peer deps. Common companions:
@hookform/resolvers— bridge toreact-hook-formdrizzle-valibot— generate valibot schemas from Drizzle tables@valibot/to-json-schema— emit JSON Schema for toolinghono,elysia,next— pair with route handlers for input validation
Alternatives
| Package | Trade-off |
|---|---|
zod | Larger ecosystem; not tree-shakable (chained methods carry the whole API). Bigger bundle. |
arktype | TypeScript-syntax-shaped runtime checks; smallest mental gap from TS types. |
yup | Older, slower, larger bundle. Still common in legacy code. |
joi | Server-side classic. Node-focused, not designed for client bundles. |
superstruct | Functional API like valibot but smaller community. |
class-validator | Decorator-based; pairs with class-transformer and NestJS. |
Real-world recipes
Object schema with safeParse
import * as v from "valibot";
const UserSchema = v.object({
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.minValue(18)),
});
const result = v.safeParse(UserSchema, { email: "alice@example.com", age: 30 });
if (result.success) {
console.log(result.output.email);
} else {
console.log(result.issues);
}
Output: logs the email when the input is valid; logs a structured issues array when not.
parse (throws on invalid input)
import * as v from "valibot";
const PostSchema = v.object({
title: v.pipe(v.string(), v.minLength(1)),
publishedAt: v.date(),
});
const post = v.parse(PostSchema, { title: "hello", publishedAt: new Date() });
Output: returns the validated object; throws ValiError on invalid input.
Pipe with transforms
pipe chains a base schema with validators and transforms.
import * as v from "valibot";
const TrimmedEmail = v.pipe(
v.string(),
v.trim(),
v.toLowerCase(),
v.email("Must be a valid email"),
);
const out = v.parse(TrimmedEmail, " ALICE@EXAMPLE.COM ");
console.log(out);
Output: alice@example.com — trimmed, lowercased, email-validated.
Discriminated union
import * as v from "valibot";
const EventSchema = v.variant("type", [
v.object({ type: v.literal("click"), x: v.number(), y: v.number() }),
v.object({ type: v.literal("submit"), values: v.record(v.string(), v.unknown()) }),
]);
const e = v.parse(EventSchema, { type: "click", x: 10, y: 20 });
Output: parser dispatches on type and returns the narrowed object.
Inferred TypeScript types
import * as v from "valibot";
const ConfigSchema = v.object({
port: v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(65535)),
host: v.string(),
ssl: v.boolean(),
});
type Config = v.InferOutput<typeof ConfigSchema>;
// { port: number; host: string; ssl: boolean }
function start(config: Config) { /* … */ }
Output: the type is derived from the schema at compile time; no manual interface duplication.
Brand types for nominal typing
import * as v from "valibot";
const UserId = v.pipe(v.string(), v.uuid(), v.brand("UserId"));
type UserId = v.InferOutput<typeof UserId>;
function findUser(id: UserId) { /* … */ }
const id = v.parse(UserId, "550e8400-e29b-41d4-a716-446655440000");
findUser(id);
Output: brand types prevent accidentally passing a plain string where a UserId is expected.
Async validation with safeParseAsync
import * as v from "valibot";
const UniqueEmail = v.pipeAsync(
v.string(),
v.email(),
v.checkAsync(async (email) => !(await db.users.findFirst({ where: { email } }))),
);
const res = await v.safeParseAsync(UniqueEmail, "alice@example.com");
Output: runs the async DB check; res.success is false if the email already exists.
Production deployment
Valibot is a library, not a server. The deployment concerns are bundle size and runtime overhead.
- Tree-shaking is the headline feature. Only the functions you
importfromvalibotend up in the bundle. A minimalv.object({ a: v.string() })schema can be ~2 KB; the full library is ~20 KB but you never ship the full library. - Edge runtimes. Valibot's ESM build runs in Cloudflare Workers, Vercel Edge, Deno Deploy, and Bun without polyfills.
v.parserfor hot paths — precompile the schema once instead of re-walking on every parse.- No native bindings. Pure JS — works everywhere.
Performance tuning
- Precompile schemas with
v.parser(or pre-build the parser inline). The hot-path validators avoid re-walking the schema each call. v.pipe(...).rawTransform()for hand-rolled transforms in a perf-critical hot path.- Avoid creating schemas in render functions. Schemas should be defined at module scope; redefining them per call defeats memoisation.
- Use
v.parse(throw) overv.safeParse(object) when failure is exceptional — throwing is slightly faster than allocating an issues array. - Validate at the boundary, not deep inside. Validate once at HTTP / queue / file ingestion; downstream code trusts the parsed type.
A useful rule of thumb: import only the schemas you need, not * as v, when bundle size is the absolute priority. Both forms tree-shake correctly with modern bundlers (esbuild, rollup, vite, turbopack); import * as v works because valibot's exports are flat re-exports of individual functions.
Version migration guide
The big migration is valibot@0.x → valibot@1.0 (mid-2025). Many APIs were renamed for consistency.
| Pre-1.0 | 1.x | Notes |
|---|---|---|
string([minLength(3)]) | pipe(string(), minLength(3)) | Validators moved out of schema args into pipe |
parse(schema, input) | parse(schema, input) | Unchanged signature |
Input<typeof S> / Output<typeof S> | InferInput<typeof S> / InferOutput<typeof S> | Renamed for clarity |
brand was a separate module | brand in core | Consolidated |
BaseSchema interface | BaseSchema (rewritten generics) | Code that types schemas directly needs revision |
Before (0.x):
import { string, email, minLength, parse } from "valibot";
const Email = string([email(), minLength(5)]);
parse(Email, "alice@example.com");
After (1.x):
import * as v from "valibot";
const Email = v.pipe(v.string(), v.email(), v.minLength(5));
v.parse(Email, "alice@example.com");
Output: identical validation behaviour; 1.x form composes cleanly via pipe.
Migration checklist:
- Run a project-wide codemod (the docs ship one) or refactor manually.
- Replace inline validator arrays with
pipe. - Update
Input/OutputtoInferInput/InferOutput. - Re-run TypeScript — schema generics tightened.
- Test the full validation surface, especially custom validators and transforms.
Security considerations
- Validate at the trust boundary. Run
v.parseon every HTTP body, query string, queue message, and file upload before letting the data into business logic. - Regex DoS.
v.regex(/expensive/)with a catastrophic pattern is a DoS vector. Test your regexes withsafe-regexor pre-emptively bound inputs withmaxLength. v.string()accepts any length. Always pair withv.maxLength(N)for user-provided strings on the server. An attacker can otherwise send 100 MB of "a"s.- Coercion (
v.coerce) can change semantics —v.coerce(v.number(), Number)turns the string "0x10" into 16, which may not be intended. Be explicit. v.unknown()andv.any()skip validation. Use them only for fields you intentionally pass through.- Don't echo validation errors to clients verbatim if they reveal internal schema names. Map to user-safe messages.
Testing & CI integration
Unit test a schema with Vitest
// schemas/user.test.ts
import { describe, it, expect } from "vitest";
import * as v from "valibot";
const UserSchema = v.object({
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.minValue(18)),
});
describe("UserSchema", () => {
it("accepts valid input", () => {
expect(v.safeParse(UserSchema, { email: "alice@example.com", age: 30 }).success).toBe(true);
});
it("rejects underage user", () => {
const r = v.safeParse(UserSchema, { email: "a@b.co", age: 16 });
expect(r.success).toBe(false);
if (!r.success) expect(r.issues[0].path?.[0].key).toBe("age");
});
});
Output: tests pass; error path identifies the failing field.
Form-resolver integration
import { useForm } from "react-hook-form";
import { valibotResolver } from "@hookform/resolvers/valibot";
const { register, handleSubmit, formState } = useForm({ resolver: valibotResolver(UserSchema) });
Output: react-hook-form validates against the valibot schema on submit.
Ecosystem integrations
| Package | Role |
|---|---|
@hookform/resolvers | valibotResolver for react-hook-form |
drizzle-valibot | Generate schemas from Drizzle table definitions |
@valibot/to-json-schema | Emit JSON Schema for OpenAPI tooling |
vee-validate | Vue form library with valibot adapter |
hono (validator middleware) | Type-safe request validation |
tRPC | Procedure input/output validation |
astro:env (Astro 5+) | Astro's typed env uses valibot internally |
t3-stack | Mentioned as a zod alternative in templates |
Troubleshooting common errors
ValiError: Invalid type — input did not match the expected type. Use safeParse and inspect result.issues for the precise path.
Property 'X' does not exist on type 'Output<...>' — schema and TypeScript types diverged. Re-derive with v.InferOutput<typeof Schema> and rebuild.
Bundle is still large — you imported with import * as v from "valibot" but the bundler is configured to preserve namespaces. Modern bundlers (Vite, Rollup, esbuild) handle this; if using Webpack 4, switch to per-symbol imports.
Async validator runs synchronously — using v.parse / v.safeParse on a pipeAsync. Switch to v.parseAsync / v.safeParseAsync.
Cannot find name 'Input' — pre-1.0 type names. Rename to InferInput / InferOutput.
v.regex accepts everything — likely passed an unanchored pattern. Add ^ and $ to anchor.
When NOT to use this
- You need the zod ecosystem. Tools like
trpc's historical defaults,openapi-zod-router, orzod-to-openapiassumezod. Valibot has equivalents but the ecosystem is smaller. - You're stuck with CJS-only tooling. Valibot is ESM-only. Old Node-CJS projects need
--experimental-vm-modulesor a build step. - You want decorators.
class-validator+ decorators feel more native in NestJS-style code. - You need JSON Schema as the source of truth.
ajvconsumes JSON Schema directly; valibot generates it but the round-trip has rough edges.
See also
- JavaScript: zod — adjacent schema library
- Concept: json — JSON Schema, validation, parsing