Valibot Usage
This skill helps you work effectively with Valibot, the modular and type-safe schema library for validating structural data.
When to use this skill
-
When the user asks about schema validation with Valibot
-
When creating or modifying Valibot schemas
-
When parsing or validating user input
-
When the user mentions Valibot, schema, or validation
-
When migrating from Zod to Valibot
CRITICAL: Valibot vs Zod — Do Not Confuse!
Valibot and Zod have different APIs. Never mix them up!
Key Differences
Feature Zod ❌ Valibot ✅
Import import { z } from 'zod'
import * as v from 'valibot'
Validations Chained methods: .email().min(5)
Pipeline: v.pipe(v.string(), v.email(), v.minLength(5))
Parsing schema.parse(data)
v.parse(schema, data)
Safe parsing schema.safeParse(data)
v.safeParse(schema, data)
Optional z.string().optional()
v.optional(v.string())
Nullable z.string().nullable()
v.nullable(v.string())
Default z.string().default('x')
v.optional(v.string(), 'x')
Transform z.string().transform(fn)
v.pipe(v.string(), v.transform(fn))
Refine/Check z.string().refine(fn)
v.pipe(v.string(), v.check(fn))
Enum z.enum(['a', 'b'])
v.picklist(['a', 'b'])
Native enum z.nativeEnum(MyEnum)
v.enum(MyEnum)
Union z.union([a, b])
v.union([a, b])
Discriminated union z.discriminatedUnion('type', [...])
v.variant('type', [...])
Intersection z.intersection(a, b)
v.intersect([a, b])
Min/max length .min(5).max(10)
v.minLength(5), v.maxLength(10)
Min/max value .gte(5).lte(10)
v.minValue(5), v.maxValue(10)
Infer type z.infer<typeof Schema>
v.InferOutput<typeof Schema>
Infer input z.input<typeof Schema>
v.InferInput<typeof Schema>
Common Mistakes to Avoid
// ❌ WRONG - This is Zod syntax, NOT Valibot! const Schema = v.string().email().min(5); const result = Schema.parse(data);
// ✅ CORRECT - Valibot uses functions and pipelines const Schema = v.pipe(v.string(), v.email(), v.minLength(5)); const result = v.parse(Schema, data);
// ❌ WRONG - Zod-style optional const Schema = v.object({ name: v.string().optional(), });
// ✅ CORRECT - Valibot wraps with optional() const Schema = v.object({ name: v.optional(v.string()), });
// ❌ WRONG - Zod-style default const Schema = v.string().default("hello");
// ✅ CORRECT - Valibot uses second argument const Schema = v.optional(v.string(), "hello");
Installation
npm install valibot # npm yarn add valibot # yarn pnpm add valibot # pnpm bun add valibot # bun
Import with a wildcard (recommended):
import * as v from "valibot";
Or with individual imports:
import { object, string, pipe, email, parse } from "valibot";
Mental Model
Valibot's API is divided into three main concepts:
- Schemas
Schemas define the expected data type. They are the starting point.
import * as v from "valibot";
// Primitive schemas const StringSchema = v.string(); const NumberSchema = v.number(); const BooleanSchema = v.boolean(); const DateSchema = v.date();
// Complex schemas const ArraySchema = v.array(v.string()); const ObjectSchema = v.object({ name: v.string(), age: v.number(), });
- Methods
Methods help you use or modify schemas. The schema is always the first argument.
// Parsing const result = v.parse(StringSchema, "hello"); const safeResult = v.safeParse(StringSchema, "hello");
// Type guard if (v.is(StringSchema, data)) { // data is typed as string }
- Actions
Actions validate or transform data within a pipe() . They MUST be used inside pipelines.
// Actions are used in pipe() const EmailSchema = v.pipe( v.string(), v.trim(), v.email(), v.endsWith("@example.com"), );
Pipelines
Pipelines extend schemas with validation and transformation actions. A pipeline always starts with a schema, followed by actions.
import * as v from "valibot";
const UsernameSchema = v.pipe( v.string(), v.trim(), v.minLength(3, "Username must be at least 3 characters"), v.maxLength(20, "Username must be at most 20 characters"), v.regex( /^[a-z0-9_]+$/i, "Username can only contain letters, numbers, and underscores", ), );
const AgeSchema = v.pipe( v.number(), v.integer("Age must be a whole number"), v.minValue(0, "Age cannot be negative"), v.maxValue(150, "Age cannot exceed 150"), );
Common Validation Actions
String validations:
-
v.email() — Valid email format
-
v.url() — Valid URL format
-
v.uuid() — Valid UUID format
-
v.regex(pattern) — Match regex pattern
-
v.minLength(n) — Minimum length
-
v.maxLength(n) — Maximum length
-
v.length(n) — Exact length
-
v.nonEmpty() — Not empty string
-
v.startsWith(str) — Starts with string
-
v.endsWith(str) — Ends with string
-
v.includes(str) — Contains string
Number validations:
-
v.minValue(n) — Minimum value (>=)
-
v.maxValue(n) — Maximum value (<=)
-
v.gtValue(n) — Greater than (>)
-
v.ltValue(n) — Less than (<)
-
v.integer() — Must be integer
-
v.finite() — Must be finite
-
v.safeInteger() — Safe integer range
-
v.multipleOf(n) — Must be multiple of n
Array validations:
-
v.minLength(n) — Minimum items
-
v.maxLength(n) — Maximum items
-
v.length(n) — Exact item count
-
v.nonEmpty() — At least one item
-
v.includes(item) — Contains item
-
v.excludes(item) — Does not contain item
Custom Validation with check()
const PasswordSchema = v.pipe( v.string(), v.minLength(8), v.check( (input) => /[A-Z]/.test(input), "Password must contain an uppercase letter", ), v.check((input) => /[0-9]/.test(input), "Password must contain a number"), );
Value Transformations
These actions modify the value without changing its type:
String transformations:
-
v.trim() — Remove leading/trailing whitespace
-
v.trimStart() — Remove leading whitespace
-
v.trimEnd() — Remove trailing whitespace
-
v.toLowerCase() — Convert to lowercase
-
v.toUpperCase() — Convert to uppercase
Number transformations:
-
v.toMinValue(n) — Clamp to minimum value (if less than n, set to n)
-
v.toMaxValue(n) — Clamp to maximum value (if greater than n, set to n)
const NormalizedEmailSchema = v.pipe( v.string(), v.trim(), v.toLowerCase(), v.email(), );
// Clamp number to range 0-100 const PercentageSchema = v.pipe(v.number(), v.toMinValue(0), v.toMaxValue(100));
Type Transformations
For converting between data types, use these built-in transformation actions:
-
v.toNumber() — Convert to number
-
v.toString() — Convert to string
-
v.toBoolean() — Convert to boolean
-
v.toBigint() — Convert to bigint
-
v.toDate() — Convert to Date
// Convert string to number const PortSchema = v.pipe(v.string(), v.toNumber(), v.integer(), v.minValue(1));
// Convert ISO string to Date const TimestampSchema = v.pipe(v.string(), v.isoDateTime(), v.toDate());
// Convert to boolean const FlagSchema = v.pipe(v.string(), v.toBoolean());
Custom Transformations
For custom transformations, use v.transform() :
const DateStringSchema = v.pipe( v.string(), v.isoDate(), v.transform((input) => new Date(input)), );
// Custom object transformation
const UserSchema = v.pipe(
v.object({
firstName: v.string(),
lastName: v.string(),
}),
v.transform((input) => ({
...input,
fullName: ${input.firstName} ${input.lastName},
})),
);
Object Schemas
Basic Object
const UserSchema = v.object({ id: v.number(), name: v.string(), email: v.pipe(v.string(), v.email()), age: v.optional(v.number()), });
type User = v.InferOutput<typeof UserSchema>;
Object Variants
// Regular object - strips unknown keys (default) const ObjectSchema = v.object({ key: v.string() });
// Loose object - allows and preserves unknown keys const LooseObjectSchema = v.looseObject({ key: v.string() });
// Strict object - throws on unknown keys const StrictObjectSchema = v.strictObject({ key: v.string() });
// Object with rest - validates unknown keys against a schema const ObjectWithRestSchema = v.objectWithRest( { key: v.string() }, v.number(), // unknown keys must be numbers );
Optional and Nullable Fields
const ProfileSchema = v.object({ // Required name: v.string(),
// Optional (can be undefined or missing) nickname: v.optional(v.string()),
// Optional with default role: v.optional(v.string(), "user"),
// Nullable (can be null) avatar: v.nullable(v.string()),
// Nullish (can be null or undefined) bio: v.nullish(v.string()),
// Nullish with default theme: v.nullish(v.string(), "light"), });
Object Methods
const BaseSchema = v.object({ id: v.number(), name: v.string(), email: v.string(), password: v.string(), });
// Pick specific keys const PublicUserSchema = v.pick(BaseSchema, ["id", "name"]);
// Omit specific keys const UserWithoutPasswordSchema = v.omit(BaseSchema, ["password"]);
// Make all optional const PartialUserSchema = v.partial(BaseSchema);
// Make all required const RequiredUserSchema = v.required(PartialUserSchema);
// Merge objects const ExtendedUserSchema = v.object({ ...BaseSchema.entries, createdAt: v.date(), });
Cross-Field Validation
const RegistrationSchema = v.pipe( v.object({ password: v.pipe(v.string(), v.minLength(8)), confirmPassword: v.string(), }), v.forward( v.partialCheck( [["password"], ["confirmPassword"]], (input) => input.password === input.confirmPassword, "Passwords do not match", ), ["confirmPassword"], ), );
Arrays and Tuples
Arrays
const TagsSchema = v.pipe( v.array(v.string()), v.minLength(1, "At least one tag required"), v.maxLength(10, "Maximum 10 tags allowed"), );
// Array of objects const UsersSchema = v.array( v.object({ id: v.number(), name: v.string(), }), );
Tuples
// Fixed-length array with specific types const CoordinatesSchema = v.tuple([v.number(), v.number()]); // Type: [number, number]
// Tuple with rest const ArgsSchema = v.tupleWithRest( [v.string()], // first arg is string v.number(), // rest are numbers ); // Type: [string, ...number[]]
Unions and Variants
Union
const StringOrNumberSchema = v.union([v.string(), v.number()]);
const StatusSchema = v.union([ v.literal("pending"), v.literal("active"), v.literal("inactive"), ]);
Picklist (for string/number literals)
// Simpler than union of literals const StatusSchema = v.picklist(["pending", "active", "inactive"]);
const PrioritySchema = v.picklist([1, 2, 3]);
Variant (discriminated union)
Use variant for better performance with discriminated unions:
const EventSchema = v.variant("type", [ v.object({ type: v.literal("click"), x: v.number(), y: v.number(), }), v.object({ type: v.literal("keypress"), key: v.string(), }), v.object({ type: v.literal("scroll"), direction: v.picklist(["up", "down"]), }), ]);
Parsing Data
parse() — Throws on Error
import * as v from "valibot";
const EmailSchema = v.pipe(v.string(), v.email());
try { const email = v.parse(EmailSchema, "jane@example.com"); console.log(email); // 'jane@example.com' } catch (error) { console.error(error); // ValiError }
safeParse() — Returns Result Object
const result = v.safeParse(EmailSchema, input);
if (result.success) { console.log(result.output); // Valid data } else { console.log(result.issues); // Array of issues }
is() — Type Guard
if (v.is(EmailSchema, input)) { // input is typed as string }
Configuration Options
// Abort early - stop at first error v.parse(Schema, data, { abortEarly: true });
// Abort pipe early - stop pipeline at first error v.parse(Schema, data, { abortPipeEarly: true });
Type Inference
import * as v from "valibot";
const UserSchema = v.object({ name: v.string(), age: v.pipe(v.string(), v.transform(Number)), role: v.optional(v.string(), "user"), });
// Output type (after transformations and defaults) type User = v.InferOutput<typeof UserSchema>; // { name: string; age: number; role: string }
// Input type (before transformations) type UserInput = v.InferInput<typeof UserSchema>; // { name: string; age: string; role?: string | undefined }
// Issue type type UserIssue = v.InferIssue<typeof UserSchema>;
Error Handling
Custom Error Messages
const LoginSchema = v.object({ email: v.pipe( v.string("Email must be a string"), v.nonEmpty("Please enter your email"), v.email("Invalid email format"), ), password: v.pipe( v.string("Password must be a string"), v.nonEmpty("Please enter your password"), v.minLength(8, "Password must be at least 8 characters"), ), });
Flattening Errors
const result = v.safeParse(LoginSchema, data);
if (!result.success) { const flat = v.flatten(result.issues); // { nested: { email: ['Invalid email format'], password: ['...'] } } }
Issue Structure
Each issue contains:
-
kind : 'schema' | 'validation' | 'transformation'
-
type : Function name (e.g., 'string', 'email', 'min_length')
-
input : The problematic input
-
expected : What was expected
-
received : What was received
-
message : Human-readable message
-
path : Array of path items for nested issues
Fallback Values
// Static fallback const NumberSchema = v.fallback(v.number(), 0); v.parse(NumberSchema, "invalid"); // Returns 0
// Dynamic fallback const DateSchema = v.fallback(v.date(), () => new Date());
Recursive Schemas
import * as v from "valibot";
type TreeNode = { value: string; children: TreeNode[]; };
const TreeNodeSchema: v.GenericSchema<TreeNode> = v.object({ value: v.string(), children: v.lazy(() => v.array(TreeNodeSchema)), });
Async Validation
For async operations (e.g., database checks), use async variants:
import * as v from "valibot";
const isUsernameAvailable = async (username: string) => { // Check database return true; };
const UsernameSchema = v.pipeAsync( v.string(), v.minLength(3), v.checkAsync(isUsernameAvailable, "Username is already taken"), );
// Must use parseAsync const username = await v.parseAsync(UsernameSchema, "john");
JSON Schema Conversion
import { toJsonSchema } from "@valibot/to-json-schema"; import * as v from "valibot";
const EmailSchema = v.pipe(v.string(), v.email()); const jsonSchema = toJsonSchema(EmailSchema); // { type: 'string', format: 'email' }
Naming Conventions
Convention 1: Same Name (Recommended for simplicity)
export const User = v.object({ name: v.string(), email: v.pipe(v.string(), v.email()), });
export type User = v.InferOutput<typeof User>;
// Usage const users: User[] = []; users.push(v.parse(User, data));
Convention 2: With Suffixes (Recommended when input/output differ)
export const UserSchema = v.object({ name: v.string(), age: v.pipe(v.string(), v.transform(Number)), });
export type UserInput = v.InferInput<typeof UserSchema>; export type UserOutput = v.InferOutput<typeof UserSchema>;
Common Patterns
Login Form
const LoginSchema = v.object({ email: v.pipe( v.string(), v.nonEmpty("Please enter your email"), v.email("Invalid email address"), ), password: v.pipe( v.string(), v.nonEmpty("Please enter your password"), v.minLength(8, "Password must be at least 8 characters"), ), });
API Response
const ApiResponseSchema = v.variant("status", [ v.object({ status: v.literal("success"), data: v.unknown(), }), v.object({ status: v.literal("error"), error: v.object({ code: v.string(), message: v.string(), }), }), ]);
Environment Variables
const EnvSchema = v.object({ NODE_ENV: v.picklist(["development", "production", "test"]), PORT: v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1)), DATABASE_URL: v.pipe(v.string(), v.url()), API_KEY: v.pipe(v.string(), v.minLength(32)), });
const env = v.parse(EnvSchema, process.env);
Date Handling
// String to Date const DateFromStringSchema = v.pipe( v.string(), v.isoDate(), v.transform((input) => new Date(input)), );
// Date validation const FutureDateSchema = v.pipe( v.date(), v.minValue(new Date(), "Date must be in the future"), );
Additional Resources
-
Valibot Documentation
-
Valibot GitHub
-
API Reference
-
Migration from Zod