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

bash
npm install valibot

Output: added valibot to dependencies

bash
pnpm add valibot

Output: added 1 package, linked from store

bash
yarn add valibot

Output: added valibot

bash
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 — valibot targets 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 to react-hook-form
  • drizzle-valibot — generate valibot schemas from Drizzle tables
  • @valibot/to-json-schema — emit JSON Schema for tooling
  • hono, elysia, next — pair with route handlers for input validation

Alternatives

PackageTrade-off
zodLarger ecosystem; not tree-shakable (chained methods carry the whole API). Bigger bundle.
arktypeTypeScript-syntax-shaped runtime checks; smallest mental gap from TS types.
yupOlder, slower, larger bundle. Still common in legacy code.
joiServer-side classic. Node-focused, not designed for client bundles.
superstructFunctional API like valibot but smaller community.
class-validatorDecorator-based; pairs with class-transformer and NestJS.

Real-world recipes

Object schema with safeParse

typescript
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)

typescript
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.

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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 import from valibot end up in the bundle. A minimal v.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.parser for 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) over v.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.xvalibot@1.0 (mid-2025). Many APIs were renamed for consistency.

Pre-1.01.xNotes
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 modulebrand in coreConsolidated
BaseSchema interfaceBaseSchema (rewritten generics)Code that types schemas directly needs revision

Before (0.x):

typescript
import { string, email, minLength, parse } from "valibot";
const Email = string([email(), minLength(5)]);
parse(Email, "alice@example.com");

After (1.x):

typescript
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:

  1. Run a project-wide codemod (the docs ship one) or refactor manually.
  2. Replace inline validator arrays with pipe.
  3. Update Input / Output to InferInput / InferOutput.
  4. Re-run TypeScript — schema generics tightened.
  5. Test the full validation surface, especially custom validators and transforms.

Security considerations

  • Validate at the trust boundary. Run v.parse on 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 with safe-regex or pre-emptively bound inputs with maxLength.
  • v.string() accepts any length. Always pair with v.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() and v.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

typescript
// 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

typescript
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

PackageRole
@hookform/resolversvalibotResolver for react-hook-form
drizzle-valibotGenerate schemas from Drizzle table definitions
@valibot/to-json-schemaEmit JSON Schema for OpenAPI tooling
vee-validateVue form library with valibot adapter
hono (validator middleware)Type-safe request validation
tRPCProcedure input/output validation
astro:env (Astro 5+)Astro's typed env uses valibot internally
t3-stackMentioned 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, or zod-to-openapi assume zod. 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-modules or 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. ajv consumes JSON Schema directly; valibot generates it but the round-trip has rough edges.

See also