mcpField
V
valibot-usage

valibot-usage

github.com/open-circle开发1662 次安装1 天前更新
代码仓库

关于 valibot-usage

面向开发者的 Valibot 使用指南,介绍这种模块化、类型安全的结构数据校验库,涵盖模式定义、解析与转换,并说明如何避免混用 Zod API。

主要功能

  1. 01通过组合式管道构建模式
  2. 02校验并转换数据
  3. 03安全解析并检查问题
  4. 04推断输入与输出类型
  5. 05处理对象、数组、联合类型及异步校验
  6. 06区分 Valibot 与 Zod 的 API

适用场景

  1. 01为用户输入定义模式
  2. 02校验并规范化数值
  3. 03将模式代码从 Zod 迁移
  4. 04检查结构化校验错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/open-circle/agent-skills --skill valibot-usage

安装后在 Claude Code 中输入/valibot-usage确认已加载。

详细介绍

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())
可为 nullz.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() — 转换为 bigint
  • v.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