valibot-usage について
モジュール型で型安全なスキーマ検証ライブラリ Valibot の開発者向けガイドです。スキーマ定義、データの解析と変換を解説し、Zod API との混同も防ぎます。
主な機能
- 01パイプラインを組み合わせたスキーマ構築
- 02データの検証と変換
- 03安全な解析と問題の確認
- 04入力型と出力型の推論
- 05オブジェクト・配列・ユニオン・非同期検証への対応
- 06Valibot と Zod の API の区別
活用シーン
- 01ユーザー入力のスキーマ定義
- 02値の検証と正規化
- 03Zod からのスキーマコード移行
- 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()) |
| Null 許容 | 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 は、次の 3 つの主な概念に分かれています。
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()— 1つ以上の要素がある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
);
オプションフィールドとNull許容フィールド
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: ['...'] } }
}
Issueの構造
各Issueには次の項目が含まれます。
kind:'schema' | 'validation' | 'transformation'type:関数名(例:'string'、'email'、'min_length')input:問題のある入力expected:期待された値received:受け取った値message:人が読めるメッセージpath:ネストされたIssueのパス項目の配列
フォールバック値
// 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 対応