Skill v1.0.1
currentAutomated scan100/1003 files
version: "1.0.1" 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
// ❌ WRONG - This is Zod syntax, NOT Valibot!const Schema = v.string().email().min(5);const result = Schema.parse(data);// ✅ CORRECT - Valibot uses functions and pipelinesconst Schema = v.pipe(v.string(), v.email(), v.minLength(5));const result = v.parse(Schema, data);
// ❌ WRONG - Zod-style optionalconst Schema = v.object({name: v.string().optional(),});// ✅ CORRECT - Valibot wraps with optional()const Schema = v.object({name: v.optional(v.string()),});
// ❌ WRONG - Zod-style defaultconst Schema = v.string().default("hello");// ✅ CORRECT - Valibot uses second argumentconst Schema = v.optional(v.string(), "hello");
Installation
npm install valibot # npmyarn add valibot # yarnpnpm add valibot # pnpmbun 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:
1. Schemas
Schemas define the expected data type. They are the starting point.
import * as v from "valibot";// Primitive schemasconst StringSchema = v.string();const NumberSchema = v.number();const BooleanSchema = v.boolean();const DateSchema = v.date();// Complex schemasconst 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.
// Parsingconst result = v.parse(StringSchema, "hello");const safeResult = v.safeParse(StringSchema, "hello");// Type guardif (v.is(StringSchema, data)) {// data is typed as string}
3. 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 formatv.url()— Valid URL formatv.uuid()— Valid UUID formatv.regex(pattern)— Match regex patternv.minLength(n)— Minimum lengthv.maxLength(n)— Maximum lengthv.length(n)— Exact lengthv.nonEmpty()— Not empty stringv.startsWith(str)— Starts with stringv.endsWith(str)— Ends with stringv.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 integerv.finite()— Must be finitev.safeInteger()— Safe integer rangev.multipleOf(n)— Must be multiple of n
Array validations:
v.minLength(n)— Minimum itemsv.maxLength(n)— Maximum itemsv.length(n)— Exact item countv.nonEmpty()— At least one itemv.includes(item)— Contains itemv.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 whitespacev.trimStart()— Remove leading whitespacev.trimEnd()— Remove trailing whitespacev.toLowerCase()— Convert to lowercasev.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-100const 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 numberv.toString()— Convert to stringv.toBoolean()— Convert to booleanv.toBigint()— Convert to bigintv.toDate()— Convert to Date
// Convert string to numberconst PortSchema = v.pipe(v.string(), v.toNumber(), v.integer(), v.minValue(1));// Convert ISO string to Dateconst TimestampSchema = v.pipe(v.string(), v.isoDateTime(), v.toDate());// Convert to booleanconst 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 transformationconst 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 keysconst LooseObjectSchema = v.looseObject({ key: v.string() });// Strict object - throws on unknown keysconst StrictObjectSchema = v.strictObject({ key: v.string() });// Object with rest - validates unknown keys against a schemaconst ObjectWithRestSchema = v.objectWithRest({ key: v.string() },v.number(), // unknown keys must be numbers);
Optional and Nullable Fields
const ProfileSchema = v.object({// Requiredname: v.string(),// Optional (can be undefined or missing)nickname: v.optional(v.string()),// Optional with defaultrole: 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 defaulttheme: 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 keysconst PublicUserSchema = v.pick(BaseSchema, ["id", "name"]);// Omit specific keysconst UserWithoutPasswordSchema = v.omit(BaseSchema, ["password"]);// Make all optionalconst PartialUserSchema = v.partial(BaseSchema);// Make all requiredconst RequiredUserSchema = v.required(PartialUserSchema);// Merge objectsconst 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 objectsconst UsersSchema = v.array(v.object({id: v.number(),name: v.string(),}),);
Tuples
// Fixed-length array with specific typesconst CoordinatesSchema = v.tuple([v.number(), v.number()]);// Type: [number, number]// Tuple with restconst ArgsSchema = v.tupleWithRest([v.string()], // first arg is stringv.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 literalsconst 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 errorv.parse(Schema, data, { abortEarly: true });// Abort pipe early - stop pipeline at first errorv.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 typetype 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 inputexpected: What was expectedreceived: What was receivedmessage: Human-readable messagepath: Array of path items for nested issues
Fallback Values
// Static fallbackconst NumberSchema = v.fallback(v.number(), 0);v.parse(NumberSchema, "invalid"); // Returns 0// Dynamic fallbackconst 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 databasereturn true;};const UsernameSchema = v.pipeAsync(v.string(),v.minLength(3),v.checkAsync(isUsernameAvailable, "Username is already taken"),);// Must use parseAsyncconst 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>;// Usageconst 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 Dateconst DateFromStringSchema = v.pipe(v.string(),v.isoDate(),v.transform((input) => new Date(input)),);// Date validationconst FutureDateSchema = v.pipe(v.date(),v.minValue(new Date(), "Date must be in the future"),);