| name | valibot |
| description | Schema validation with Valibot, the modular and type-safe schema library. Use when the user needs to validate data, create schemas, parse inputs, or work with Valibot in their project. Also use when migrating from Zod to Valibot. |
| license | MIT |
| metadata | {"author":"open-circle","version":"1.0"} |
Valibot
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
const Schema = v.string().email().min(5);
const result = Schema.parse(data);
const Schema = v.pipe(v.string(), v.email(), v.minLength(5));
const result = v.parse(Schema, data);
const Schema = v.object({
name: v.string().optional(),
});
const Schema = v.object({
name: v.optional(v.string()),
});
const Schema = v.string().default("hello");
const Schema = v.optional(v.string(), "hello");
Installation
npm install valibot
yarn add valibot
pnpm add valibot
bun add valibot
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:
1. Schemas
Schemas define the expected data type. They are the starting point.
import * as v from "valibot";
const StringSchema = v.string();
const NumberSchema = v.number();
const BooleanSchema = v.boolean();
const DateSchema = v.date();
const ArraySchema = v.array(v.string());
const ObjectSchema = v.object({
name: v.string(),
age: v.number(),
});
2. Methods
Methods help you use or modify schemas. The schema is always the first argument.
const result = v.parse(StringSchema, "hello");
const safeResult = v.safeParse(StringSchema, "hello");
if (v.is(StringSchema, data)) {
}
3. Actions
Actions validate or transform data within a pipe(). They MUST be used inside pipelines.
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(),
);
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
const PortSchema = v.pipe(v.string(), v.toNumber(), v.integer(), v.minValue(1));
const TimestampSchema = v.pipe(v.string(), v.isoDateTime(), v.toDate());
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)),
);
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
const ObjectSchema = v.object({ key: v.string() });
const LooseObjectSchema = v.looseObject({ key: v.string() });
const StrictObjectSchema = v.strictObject({ key: v.string() });
const ObjectWithRestSchema = v.objectWithRest(
{ key: v.string() },
v.number(),
);
Optional and Nullable Fields
const ProfileSchema = v.object({
name: v.string(),
nickname: v.optional(v.string()),
role: v.optional(v.string(), "user"),
avatar: v.nullable(v.string()),
bio: v.nullish(v.string()),
theme: v.nullish(v.string(), "light"),
});
Object Methods
const BaseSchema = v.object({
id: v.number(),
name: v.string(),
email: v.string(),
password: v.string(),
});
const PublicUserSchema = v.pick(BaseSchema, ["id", "name"]);
const UserWithoutPasswordSchema = v.omit(BaseSchema, ["password"]);
const PartialUserSchema = v.partial(BaseSchema);
const RequiredUserSchema = v.required(PartialUserSchema);
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"),
);
const UsersSchema = v.array(
v.object({
id: v.number(),
name: v.string(),
}),
);
Tuples
const CoordinatesSchema = v.tuple([v.number(), v.number()]);
const ArgsSchema = v.tupleWithRest(
[v.string()],
v.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)
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);
} catch (error) {
console.error(error);
}
safeParse() — Returns Result Object
const result = v.safeParse(EmailSchema, input);
if (result.success) {
console.log(result.output);
} else {
console.log(result.issues);
}
is() — Type Guard
if (v.is(EmailSchema, input)) {
}
Configuration Options
v.parse(Schema, data, { abortEarly: true });
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"),
});
type User = v.InferOutput<typeof UserSchema>;
type UserInput = v.InferInput<typeof UserSchema>;
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);
}
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
const NumberSchema = v.fallback(v.number(), 0);
v.parse(NumberSchema, "invalid");
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) => {
return true;
};
const UsernameSchema = v.pipeAsync(
v.string(),
v.minLength(3),
v.checkAsync(isUsernameAvailable, "Username is already taken"),
);
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);
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>;
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
const DateFromStringSchema = v.pipe(
v.string(),
v.isoDate(),
v.transform((input) => new Date(input)),
);
const FutureDateSchema = v.pipe(
v.date(),
v.minValue(new Date(), "Date must be in the future"),
);
Additional Resources