Valibot is a modular schema validation library — its tree-shakeable design means only the validators you import are bundled, typically 1–2 KB gzipped compared to Zod’s ~14 KB. object({}), string(), number(), array(), and union() build schemas composably. pipe(schema, ...actions) chains transforms and refinements — transform, minLength, email, regex, check. parse(schema, data) throws on validation failure; safeParse(schema, data) returns { success, output, issues }. InferOutput<typeof schema> extracts the TypeScript type. flatten(issues) formats errors into nested field paths for form libraries. Valibot integrates with @hookform/resolvers/valibot for React Hook Form, and @hono/valibot-validator for Hono route validation. Claude Code generates Valibot schema definitions, pipe transforms, form integrations, and the API validation middleware for TypeScript applications.
CLAUDE.md for Valibot
## Valibot Stack
- Version: valibot >= 0.42
- Schema: v.object, v.string, v.number, v.boolean, v.array, v.union, v.literal
- Transforms: v.pipe(schema, v.transform(fn)) — convert value after parse
- Refinements: v.pipe(schema, v.check(fn, "message")) — custom validate
- String: v.email(), v.url(), v.minLength(n), v.maxLength(n), v.regex(re), v.trim()
- Number: v.minValue(n), v.maxValue(n), v.integer(), v.multipleOf(n)
- Errors: v.safeParse(schema, data) → { success, output, issues }
- Types: v.InferOutput<typeof schema> — extract TypeScript type
Schema Definitions
// lib/schemas/order.ts — Valibot schemas
import * as v from "valibot"
// Reusable primitive schemas
const UUIDSchema = v.pipe(
v.string(),
v.uuid("Must be a valid UUID")
)
const PositiveIntSchema = v.pipe(
v.number(),
v.integer("Must be a whole number"),
v.minValue(1, "Must be positive")
)
const CentsSchema = v.pipe(
v.number(),
v.integer("Cents must be a whole number"),
v.minValue(0, "Amount cannot be negative")
)
const EmailSchema = v.pipe(
v.string(),
v.trim(),
v.email("Must be a valid email address"),
v.maxLength(254, "Email too long")
)
// Order item schema
export const OrderItemSchema = v.object({
productId: UUIDSchema,
name: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
quantity: PositiveIntSchema,
priceCents: CentsSchema,
})
// Create order input
export const CreateOrderInputSchema = v.object({
items: v.pipe(
v.array(OrderItemSchema),
v.minLength(1, "Order must have at least one item"),
v.maxLength(50, "Order cannot have more than 50 items")
),
shippingAddress: v.object({
line1: v.pipe(v.string(), v.minLength(1, "Required"), v.maxLength(100)),
line2: v.optional(v.pipe(v.string(), v.maxLength(100))),
city: v.pipe(v.string(), v.minLength(1, "Required"), v.maxLength(50)),
postalCode: v.pipe(
v.string(),
v.regex(/^[A-Z0-9]{3,10}$/i, "Invalid postal code format")
),
country: v.pipe(
v.string(),
v.length(2, "Must be a 2-letter country code"),
v.toUpperCase()
),
}),
couponCode: v.optional(
v.pipe(
v.string(),
v.trim(),
v.toUpperCase(),
v.regex(/^[A-Z0-9]{4,16}$/, "Invalid coupon code format")
)
),
})
// Order status enum
export const OrderStatusSchema = v.picklist(
["pending", "processing", "shipped", "delivered", "cancelled"],
"Invalid order status"
)
// Full order schema (from DB / API response)
export const OrderSchema = v.object({
id: UUIDSchema,
customerId: UUIDSchema,
status: OrderStatusSchema,
items: v.array(OrderItemSchema),
totalCents: CentsSchema,
couponCode: v.nullable(v.string()),
createdAt: v.pipe(
v.string(),
v.isoTimestamp("Must be a valid ISO timestamp"),
v.transform(s => new Date(s))
),
})
// Export TypeScript types
export type OrderItem = v.InferOutput<typeof OrderItemSchema>
export type CreateOrderInput = v.InferOutput<typeof CreateOrderInputSchema>
export type OrderStatus = v.InferOutput<typeof OrderStatusSchema>
export type Order = v.InferOutput<typeof OrderSchema>
Parse and SafeParse
// lib/validation.ts — parsing utilities
import * as v from "valibot"
import type { BaseSchema, BaseIssue } from "valibot"
// Strict parse — throws ValiError on failure
export function parseStrict<T>(schema: BaseSchema<unknown, T, BaseIssue<unknown>>, data: unknown): T {
return v.parse(schema, data)
}
// Safe parse — returns result object
export function parseInput<T>(
schema: BaseSchema<unknown, T, BaseIssue<unknown>>,
data: unknown
): { success: true; data: T } | { success: false; errors: Record<string, string[]> } {
const result = v.safeParse(schema, data)
if (result.success) {
return { success: true, data: result.output }
}
// Flatten issues into { fieldPath: ["error messages"] }
const flat = v.flatten<typeof schema>(result.issues)
const errors: Record<string, string[]> = {}
// Root-level errors
if (flat.root) {
errors["_root"] = flat.root
}
// Nested field errors
if (flat.nested) {
for (const [path, messages] of Object.entries(flat.nested)) {
if (messages) errors[path] = messages
}
}
return { success: false, errors }
}
// Example usage
import { CreateOrderInputSchema } from "./schemas/order"
const result = parseInput(CreateOrderInputSchema, {
items: [{ productId: "not-a-uuid", quantity: 0, priceCents: -1, name: "" }],
shippingAddress: { line1: "123 Main St", city: "NYC", postalCode: "10001", country: "us" },
})
if (!result.success) {
console.log(result.errors)
// {
// "items.0.productId": ["Must be a valid UUID"],
// "items.0.quantity": ["Must be positive"],
// "items.0.priceCents": ["Amount cannot be negative"],
// "items.0.name": ["Too short"]
// }
}
React Hook Form Integration
// components/CreateOrderForm.tsx — with @hookform/resolvers/valibot
import { useForm } from "react-hook-form"
import { valibotResolver } from "@hookform/resolvers/valibot"
import { CreateOrderInputSchema, type CreateOrderInput } from "@/lib/schemas/order"
export function CreateOrderForm({ onSuccess }: { onSuccess: (order: Order) => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<CreateOrderInput>({
resolver: valibotResolver(CreateOrderInputSchema),
defaultValues: {
items: [],
shippingAddress: { country: "US" },
},
})
const onSubmit = async (data: CreateOrderInput) => {
const response = await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
if (!response.ok) {
const err = await response.json()
throw new Error(err.message)
}
onSuccess(await response.json())
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register("shippingAddress.line1")}
placeholder="Address line 1"
/>
{errors.shippingAddress?.line1 && (
<p className="error">{errors.shippingAddress.line1.message}</p>
)}
</div>
<div>
<input
{...register("shippingAddress.city")}
placeholder="City"
/>
{errors.shippingAddress?.city && (
<p className="error">{errors.shippingAddress.city.message}</p>
)}
</div>
<div>
<input
{...register("shippingAddress.country")}
placeholder="Country (2-letter code)"
maxLength={2}
/>
{errors.shippingAddress?.country && (
<p className="error">{errors.shippingAddress.country.message}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Placing order..." : "Place Order"}
</button>
</form>
)
}
Hono Route Validation
// src/routes/orders.ts — Hono with valibot validator
import { Hono } from "hono"
import { vValidator } from "@hono/valibot-validator"
import * as v from "valibot"
import { CreateOrderInputSchema } from "../lib/schemas/order"
const orders = new Hono()
orders.post(
"/",
vValidator("json", CreateOrderInputSchema, (result, c) => {
if (!result.success) {
const flat = v.flatten(result.issues)
return c.json(
{ error: "Validation failed", issues: flat.nested ?? flat.root ?? {} },
422
)
}
}),
async (c) => {
// result.output is typed as CreateOrderInput
const input = c.req.valid("json")
const totalCents = input.items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0
)
const order = await createOrder({ ...input, totalCents })
return c.json(order, 201)
}
)
// Query param validation
const OrderQuerySchema = v.object({
status: v.optional(v.picklist(["pending", "shipped", "delivered"])),
limit: v.optional(
v.pipe(
v.string(),
v.transform(Number),
v.integer(),
v.minValue(1),
v.maxValue(100)
)
),
})
orders.get(
"/",
vValidator("query", OrderQuerySchema),
async (c) => {
const { status, limit = 20 } = c.req.valid("query")
const results = await listOrders({ status, limit })
return c.json(results)
}
)
export { orders }
Discriminated Union / Variant Schemas
// lib/schemas/events.ts — union schemas for event types
import * as v from "valibot"
const OrderCreatedSchema = v.object({
type: v.literal("order.created"),
orderId: v.pipe(v.string(), v.uuid()),
customerId: v.pipe(v.string(), v.uuid()),
totalCents: v.pipe(v.number(), v.integer(), v.minValue(0)),
})
const OrderShippedSchema = v.object({
type: v.literal("order.shipped"),
orderId: v.pipe(v.string(), v.uuid()),
trackingNumber: v.string(),
carrier: v.picklist(["FEDEX", "UPS", "USPS", "DHL"]),
})
const OrderCancelledSchema = v.object({
type: v.literal("order.cancelled"),
orderId: v.pipe(v.string(), v.uuid()),
reason: v.optional(v.string()),
})
export const OrderEventSchema = v.variant("type", [
OrderCreatedSchema,
OrderShippedSchema,
OrderCancelledSchema,
])
export type OrderEvent = v.InferOutput<typeof OrderEventSchema>
// Narrow with discriminant
function handleEvent(event: OrderEvent) {
switch (event.type) {
case "order.created":
console.log(event.totalCents) // TypeScript knows this exists
break
case "order.shipped":
console.log(event.trackingNumber) // TypeScript knows this exists
break
}
}
For the Zod alternative that has broader ecosystem adoption with more third-party integrations (tRPC, Drizzle, TanStack Form), larger bundle size but more mature API surface, and better error messages out of the box, the tRPC guide covers Zod-based input validation. For the ArkType alternative that provides even more expressive syntax with runtime type assertions similar to TypeScript syntax, the TypeScript-native validation patterns are available in the TypeScript utilities guide. The Claude Skills 360 bundle includes Valibot skill sets covering schema composition, transforms, and form integration. Start with the free tier to try Valibot schema generation.