关于 valibot
Valibot 是用于验证和转换结构化数据的模块化、类型安全 Schema 库。此技能帮助开发者构建 Schema、解析输入,并区分其函数式管道 API 与 Zod 的链式 API。
主要功能
- 01为结构化数据构建 Schema
- 02通过可组合管道执行验证
- 03安全解析或在错误时抛出异常
- 04转换并规范化值
- 05推断输入和输出类型
- 06处理嵌套验证问题
适用场景
- 01验证表单或 API 输入
- 02转换并规范化解析后的值
- 03定义对象、数组或联合 Schema
- 04将验证代码从 Zod 迁移
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ open-circle/ agent-skills --skill valibot安装后在 Claude Code 中输入/valibot确认已加载。
附带文件.claude-plugin/plugin.json
详细介绍
Valibot
此技能可帮助你高效使用 Valibot,这是一个用于验证结构化数据的模块化、类型安全的模式库。
何时使用此技能
- 用户询问如何使用 Valibot 验证模式时
- 创建或修改 Valibot 模式时
- 解析或验证用户输入时
- 用户提到 Valibot、模式或验证时
- 从 Zod 迁移到 Valibot 时
重要:Valibot 与 Zod 的区别——切勿混淆!
Valibot 和 Zod 的 API 不同,切勿混用!
主要区别
| 功能 | Zod ❌ | Valibot ✅ |
|---|---|---|
| 导入 | import { z } from 'zod' | import * as v from 'valibot' |
| 验证 | 链式方法:.email().min(5) | 管道:v.pipe(v.string(), v.email(), v.minLength(5)) |
| 解析 | schema.parse(data) | v.parse(schema, data) |
| 安全解析 | schema.safeParse(data) | v.safeParse(schema, data) |
| 可选 | z.string().optional() | v.optional(v.string()) |
| 可空 | z.string().nullable() | v.nullable(v.string()) |
| 默认值 | z.string().default('x') | v.optional(v.string(), 'x') |
| 转换 | z.string().transform(fn) | v.pipe(v.string(), v.transform(fn)) |
| 精化/检查 | z.string().refine(fn) | v.pipe(v.string(), v.check(fn)) |
| 枚举 | z.enum(['a', 'b']) | v.picklist(['a', 'b']) |
| 原生枚举 | z.nativeEnum(MyEnum) | v.enum(MyEnum) |
| 联合 | z.union([a, b]) | v.union([a, b]) |
| 判别联合 | z.discriminatedUnion('type', [...]) | v.variant('type', [...]) |
| 交集 | z.intersection(a, b) | v.intersect([a, b]) |
| 最小/最大长度 | .min(5).max(10) | v.minLength(5), v.maxLength(10) |
| 最小/最大值 | .gte(5).lte(10) | v.minValue(5), v.maxValue(10) |
| 推断类型 | z.infer<typeof Schema> | v.InferOutput<typeof Schema> |
| 推断输入 | z.input<typeof Schema> | v.InferInput<typeof Schema> |
应避免的常见错误
// ❌ 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");
安装
npm install valibot # npm
yarn add valibot # yarn
pnpm add valibot # pnpm
bun add valibot # bun
使用通配符导入(推荐):
import * as v from "valibot";
或单独导入:
import { object, string, pipe, email, parse } from "valibot";
心智模型
Valibot 的 API 分为三个主要概念:
1. 模式
模式定义预期的数据类型,是操作的起点。
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(),
});
2. 方法
方法用于使用或修改模式。模式始终是第一个参数。
// 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
}
3. 操作
操作会在 pipe() 中验证或转换数据。必须在管道中使用。
// Actions are used in pipe()
const EmailSchema = v.pipe(
v.string(),
v.trim(),
v.email(),
v.endsWith("@example.com"),
);
管道
管道通过验证和转换操作扩展模式。管道始终以模式开头,后接操作。
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"),
);
常见验证操作
字符串验证:
v.email()— 邮箱格式有效v.url()— URL 格式有效v.uuid()— UUID 格式有效v.regex(pattern)— 匹配正则表达式模式v.minLength(n)— 最小长度v.maxLength(n)— 最大长度v.length(n)— 指定长度v.nonEmpty()— 字符串不能为空v.startsWith(str)— 以指定字符串开头v.endsWith(str)— 以指定字符串结尾v.includes(str)— 包含指定字符串
数字验证:
v.minValue(n)— 最小值(>=)v.maxValue(n)— 最大值(<=)v.gtValue(n)— 大于(>)v.ltValue(n)— 小于(<)v.integer()— 必须为整数v.finite()— 必须为有限数v.safeInteger()— 安全整数范围v.multipleOf(n)— 必须是 n 的倍数
数组验证:
v.minLength(n)— 最少项数v.maxLength(n)— 最多项数v.length(n)— 指定项数v.nonEmpty()— 至少包含一项v.includes(item)— 包含指定项v.excludes(item)— 不包含指定项
使用 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"),
);
值转换
这些操作会修改值,但不会改变其类型:
字符串转换:
v.trim()— 移除首尾空白字符v.trimStart()— 移除开头的空白字符v.trimEnd()— 移除结尾的空白字符v.toLowerCase()— 转换为小写v.toUpperCase()— 转换为大写
数字转换:
v.toMinValue(n)— 限定最小值(如果小于 n,则设为 n)v.toMaxValue(n)— 限定最大值(如果大于 n,则设为 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));
类型转换
要在数据类型之间转换,请使用以下内置转换操作:
v.toNumber()— 转换为数字v.toString()— 转换为字符串v.toBoolean()— 转换为布尔值v.toBigint()— 转换为 bigintv.toDate()— 转换为 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());
自定义转换
要进行自定义转换,请使用 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}`,
})),
);
对象架构
基本对象
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>;
对象变体
// 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
);
可选和可空字段
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"),
});
对象方法
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(),
});
跨字段验证
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"],
),
);
数组和元组
数组
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(),
}),
);
元组
// 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[]]
联合类型和变体
联合类型
const StringOrNumberSchema = v.union([v.string(), v.number()]);
const StatusSchema = v.union([
v.literal("pending"),
v.literal("active"),
v.literal("inactive"),
]);
选项列表(用于字符串/数字字面量)
// Simpler than union of literals
const StatusSchema = v.picklist(["pending", "active", "inactive"]);
const PrioritySchema = v.picklist([1, 2, 3]);
变体(可辨识联合类型)
对于可辨识联合类型,请使用 variant 以获得更好的性能:
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"]),
}),
]);
解析数据
parse() — 出错时抛出异常
import * as v from "valibot";
const EmailSchema = v.pipe(v.string(), v.email());
try {
const email = v.parse(EmailSchema, "[email protected]");
console.log(email); // '[email protected]'
} catch (error) {
console.error(error); // ValiError
}
safeParse() — 返回结果对象
const result = v.safeParse(EmailSchema, input);
if (result.success) {
console.log(result.output); // Valid data
} else {
console.log(result.issues); // Array of issues
}
is() — 类型守卫
if (v.is(EmailSchema, input)) {
// input is typed as string
}
配置选项
// 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 });
类型推断
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>;
错误处理
自定义错误消息
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"),
),
});
展平错误
const result = v.safeParse(LoginSchema, data);
if (!result.success) {
const flat = v.flatten(result.issues);
// { nested: { email: ['Invalid email format'], password: ['...'] } }
}
问题结构
每个问题都包含:
kind:'schema' | 'validation' | 'transformation'type:函数名称(例如,'string'、'email'、'min_length')input:有问题的输入expected:预期内容received:实际收到的内容message:易于理解的消息path:嵌套问题的路径项数组
回退值
// 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());
递归架构
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)),
});
异步验证
对于异步操作(例如,数据库检查),请使用异步变体:
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 转换
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' }
命名约定
约定 1:名称相同(简洁性优先时推荐)
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));
约定 2:使用后缀(输入/输出不同时推荐)
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>;
常见模式
登录表单
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 响应
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(),
}),
}),
]);
环境变量
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);
日期处理
// 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"),
);
其他资源
评论
支持 Markdown