Valibot
ライブラリ
Valibot
概要
Valibotは「モジュラーで型安全なスキーマライブラリ」として開発された、次世代TypeScriptバリデーションライブラリです。Zod、Ajv、Joi、Yupと類似の機能を提供しながら、革新的なモジュラー設計により劇的なバンドルサイズ削減を実現します。最小700バイト未満から開始し、tree shakingとコード分割により、Zodと比較して最大95%のバンドルサイズ削減が可能です。2025年現在、モダンWebアプリケーションのパフォーマンス要求に応える最先端のバリデーションソリューションです。
詳細
Valibot 0.30系は2025年現在の最新版で、外部依存関係なしでJavaScript環境で動作します。従来の巨大な関数とメソッドチェーンに頼らず、小さく独立した関数の組み合わせによるAPI設計を採用。100%のテストカバレッジにより信頼性を保証し、React、Vue、Svelte、NestJS、DrizzleORM等の幅広いエコシステムサポートを提供します。パイプライン形式のバリデーション(最大20アクション)により、チェックと変換を柔軟に組み合わせ可能です。
主な特徴
- 極小バンドルサイズ: tree shakingにより700バイト未満から開始
- 完全な型安全性: TypeScript静的型推論との完全統合
- モジュラー設計: 必要な機能のみをインポートする革新的アーキテクチャ
- パイプライン形式バリデーション: アクションの組み合わせによる柔軟な処理
- ゼロ依存関係: 外部ライブラリに依存しない純粋な実装
- 豊富なエコシステム: 主要フレームワークとの幅広い統合
メリット・デメリット
メリット
- Zodより最大95%小さいバンドルサイズ
- モダンWebアプリケーションに最適化されたパフォーマンス
- 100%テストカバレッジによる高い信頼性
- TypeScriptファーストの開発体験
- tree shakingによる完全な最適化
- 主要フレームワークエコシステムとの統合
デメリット
- 比較的新しいライブラリで情報が限定的
- Zodに比べて採用事例がまだ少ない
- 学習コストがやや高い(新しいパイプライン概念)
- エコシステムがまだ成長中
- 複雑なスキーマでの設計パターンが発展途上
- プロダクション使用時の長期サポート実績が少ない
参考ページ
書き方の例
インストールと基本セットアップ
# Valibotのインストール
npm install valibot
yarn add valibot
pnpm add valibot
bun add valibot
# TypeScript 4.7以上が必要
# 依存関係なしの軽量ライブラリ
基本的なスキーマ定義とバリデーション
import {
object,
string,
number,
email,
minLength,
maxLength,
parse,
safeParse,
pipe,
InferInput,
InferOutput
} from 'valibot';
// 基本的なスキーマ定義(モジュラー形式)
const UserSchema = object({
name: pipe(
string(),
minLength(2, '名前は2文字以上である必要があります'),
maxLength(50, '名前は50文字以下である必要があります')
),
age: pipe(
number(),
minValue(0, '年齢は0以上である必要があります'),
maxValue(150, '年齢は150以下である必要があります')
),
email: pipe(
string(),
email('有効なメールアドレスを入力してください')
)
});
// 型推論(入力型と出力型)
type UserInput = InferInput<typeof UserSchema>;
type UserOutput = InferOutput<typeof UserSchema>;
// データのバリデーション
const userData = {
name: '田中太郎',
age: 30,
email: '[email protected]'
};
try {
// parse: 失敗時は例外をthrow
const validUser = parse(UserSchema, userData);
console.log('バリデーション成功:', validUser);
} catch (error) {
console.error('バリデーションエラー:', error);
}
// safeParse: 失敗時も例外を投げない
const result = safeParse(UserSchema, userData);
if (result.success) {
console.log('バリデーション成功:', result.output);
} else {
console.log('バリデーション失敗:');
result.issues.forEach(issue => {
console.log(`${issue.path?.join('.')}: ${issue.message}`);
});
}
// プリミティブ型のバリデーション
import { boolean, date } from 'valibot';
const stringSchema = string();
const numberSchema = number();
const booleanSchema = boolean();
const dateSchema = date();
console.log(parse(stringSchema, "Hello")); // "Hello"
console.log(parse(numberSchema, 42)); // 42
console.log(parse(booleanSchema, true)); // true
console.log(parse(dateSchema, new Date())); // Date object
高度なバリデーションとパイプライン処理
import {
object,
string,
number,
array,
optional,
nullable,
pipe,
regex,
transform,
custom,
union,
literal,
minLength,
maxLength,
minValue,
maxValue,
email,
url
} from 'valibot';
// 複雑なパイプラインバリデーション
const PasswordSchema = pipe(
string(),
minLength(8, 'パスワードは8文字以上である必要があります'),
maxLength(128, 'パスワードは128文字以下である必要があります'),
regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'パスワードは大文字、小文字、数字、特殊文字を含む必要があります'
)
);
// カスタムバリデーション
const EvenNumberSchema = pipe(
number(),
custom(
(value) => value % 2 === 0,
'偶数である必要があります'
)
);
// 変換を含むパイプライン
const TrimmedStringSchema = pipe(
string(),
transform((value) => value.trim()),
minLength(1, 'トリム後に空文字は許可されません')
);
// Union型(複数の型の選択肢)
const StatusSchema = union([
literal('pending'),
literal('approved'),
literal('rejected')
]);
// Optional とNullable
const UserProfileSchema = object({
id: string(),
name: string(),
bio: optional(pipe(string(), maxLength(500))),
avatar: nullable(pipe(string(), url())),
age: optional(pipe(number(), minValue(13), maxValue(120))),
status: StatusSchema
});
// 配列のバリデーション
const TagsSchema = pipe(
array(pipe(string(), minLength(1), maxLength(20))),
minLength(1, '最低1つのタグが必要です'),
maxLength(10, '最大10個のタグまで許可されます')
);
// ネストしたオブジェクト
const AddressSchema = object({
street: pipe(string(), minLength(5)),
city: string(),
postalCode: pipe(
string(),
regex(/^\d{3}-\d{4}$/, '郵便番号は123-4567の形式で入力してください')
),
country: pipe(string(), minLength(2))
});
const ComplexUserSchema = object({
personal: object({
firstName: pipe(string(), minLength(1)),
lastName: pipe(string(), minLength(1)),
email: pipe(string(), email())
}),
address: AddressSchema,
tags: TagsSchema,
preferences: object({
newsletter: boolean(),
theme: union([literal('light'), literal('dark'), literal('auto')])
})
});
// 複雑なデータのバリデーション
const complexData = {
personal: {
firstName: '太郎',
lastName: '田中',
email: '[email protected]'
},
address: {
street: '渋谷区道玄坂1-2-3',
city: '東京',
postalCode: '150-0043',
country: 'Japan'
},
tags: ['developer', 'typescript', 'javascript'],
preferences: {
newsletter: true,
theme: 'dark' as const
}
};
const complexResult = safeParse(ComplexUserSchema, complexData);
if (complexResult.success) {
console.log('複雑なバリデーション成功:', complexResult.output);
} else {
console.log('複雑なバリデーションエラー:');
complexResult.issues.forEach(issue => {
console.log(`フィールド: ${issue.path?.join('.')}`);
console.log(`メッセージ: ${issue.message}`);
});
}
フレームワーク統合(React、Vue、NestJS等)
// React Hook Formとの統合
import { useForm } from 'react-hook-form';
import { valibotResolver } from '@hookform/resolvers/valibot';
import { object, string, number, pipe, minLength, email, minValue } from 'valibot';
const FormSchema = object({
name: pipe(string(), minLength(1, '名前は必須です')),
email: pipe(string(), email('有効なメールアドレスを入力してください')),
age: pipe(number(), minValue(18, '18歳以上である必要があります'))
});
function UserForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: valibotResolver(FormSchema)
});
const onSubmit = (data: InferOutput<typeof FormSchema>) => {
console.log('送信データ:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} placeholder="名前" />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} placeholder="メールアドレス" type="email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('age', { valueAsNumber: true })} placeholder="年齢" type="number" />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">送信</button>
</form>
);
}
// Express.jsでのミドルウェア例
import express from 'express';
import { object, string, pipe, minLength, email } from 'valibot';
const app = express();
app.use(express.json());
const CreateUserSchema = object({
name: pipe(string(), minLength(1)),
email: pipe(string(), email()),
password: pipe(string(), minLength(8))
});
// バリデーションミドルウェア
const validateRequest = (schema: any) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const result = safeParse(schema, req.body);
if (!result.success) {
return res.status(400).json({
error: 'バリデーションエラー',
issues: result.issues.map(issue => ({
field: issue.path?.join('.'),
message: issue.message
}))
});
}
req.body = result.output; // 正規化されたデータを設定
next();
};
};
app.post('/users', validateRequest(CreateUserSchema), (req, res) => {
res.json({ message: 'ユーザー作成成功', user: req.body });
});
// NestJS DTOとの統合例
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import { BaseSchema, safeParse } from 'valibot';
@Injectable()
export class ValibotValidationPipe implements PipeTransform {
constructor(private schema: BaseSchema<any, any, any>) {}
transform(value: any) {
const result = safeParse(this.schema, value);
if (!result.success) {
const errorMessages = result.issues.map(issue =>
`${issue.path?.join('.')}: ${issue.message}`
);
throw new BadRequestException({
message: 'バリデーションエラー',
errors: errorMessages
});
}
return result.output;
}
}
// NestJS Controllerでの使用
import { Controller, Post, Body } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
async createUser(@Body(new ValibotValidationPipe(CreateUserSchema)) userData: InferOutput<typeof CreateUserSchema>) {
return { message: 'ユーザーが作成されました', user: userData };
}
}
カスタムバリデーションと高度な機能
import {
custom,
transform,
pipe,
string,
number,
object,
array,
forward,
partialCheck,
BaseSchema,
BaseIssue
} from 'valibot';
// カスタムバリデーション関数
const isUniqueEmail = (email: string): boolean => {
// 実際のアプリケーションでは、データベースをチェック
const existingEmails = ['[email protected]', '[email protected]'];
return !existingEmails.includes(email.toLowerCase());
};
// 非同期カスタムバリデーション(Valibotは同期のみだが、パターンを示す)
const EmailUniqueSchema = pipe(
string(),
email(),
custom(
(email) => isUniqueEmail(email),
'このメールアドレスは既に使用されています'
)
);
// 複雑な条件付きバリデーション
const ConditionalUserSchema = object({
accountType: union([literal('personal'), literal('business')]),
name: string(),
companyName: optional(string()),
taxId: optional(string())
});
// 全体のオブジェクトバリデーション
const BusinessAccountSchema = pipe(
ConditionalUserSchema,
partialCheck(
[['accountType'], ['companyName'], ['taxId']],
(input) => {
if (input.accountType === 'business') {
return !!(input.companyName && input.taxId);
}
return true;
},
'ビジネスアカウントでは会社名と税務IDが必要です'
)
);
// データ変換を含むスキーマ
const UserInputSchema = object({
name: pipe(
string(),
transform(name => name.trim()),
transform(name => name.replace(/\s+/g, ' ')), // 複数空白を単一空白に
minLength(1)
),
email: pipe(
string(),
transform(email => email.toLowerCase()),
email()
),
tags: pipe(
string(),
transform(tags => tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0)),
transform(tags => [...new Set(tags)]) // 重複除去
)
});
// 再帰的スキーマ(自己参照)
type Category = {
id: string;
name: string;
parent?: Category;
children: Category[];
};
const CategorySchema: BaseSchema<any, Category, BaseIssue<unknown>> = object({
id: string(),
name: string(),
parent: optional(lazy(() => CategorySchema)),
children: array(lazy(() => CategorySchema))
});
// パフォーマンス最適化されたスキーマ
const OptimizedSchema = object({
// 基本的なバリデーションのみで高速
id: string(),
name: string(),
// 複雑なバリデーションは必要な場合のみ
email: conditional(
(input) => typeof input === 'string' && input.includes('@'),
pipe(string(), email()),
string()
)
});
// バリデーションエラーのカスタマイズ
const CustomErrorSchema = object({
username: pipe(
string(),
minLength(3, 'ユーザー名は3文字以上必要です'),
maxLength(20, 'ユーザー名は20文字以下である必要があります'),
regex(
/^[a-zA-Z0-9_]+$/,
'ユーザー名は英数字とアンダースコアのみ使用可能です'
),
custom(
(username) => !['admin', 'root', 'system'].includes(username.toLowerCase()),
'このユーザー名は予約されています'
)
)
});
// バリデーション結果の詳細解析
function analyzeValidationResult<T>(
schema: BaseSchema<any, T, any>,
data: unknown
): {
isValid: boolean;
data?: T;
errorSummary?: {
totalErrors: number;
fieldErrors: Record<string, string[]>;
globalErrors: string[];
};
} {
const result = safeParse(schema, data);
if (result.success) {
return {
isValid: true,
data: result.output
};
}
const fieldErrors: Record<string, string[]> = {};
const globalErrors: string[] = [];
result.issues.forEach(issue => {
if (issue.path && issue.path.length > 0) {
const fieldPath = issue.path.join('.');
if (!fieldErrors[fieldPath]) {
fieldErrors[fieldPath] = [];
}
fieldErrors[fieldPath].push(issue.message);
} else {
globalErrors.push(issue.message);
}
});
return {
isValid: false,
errorSummary: {
totalErrors: result.issues.length,
fieldErrors,
globalErrors
}
};
}
// 使用例
const testData = {
accountType: 'business' as const,
name: ' 山田 太郎 ',
companyName: '',
taxId: ''
};
const analysis = analyzeValidationResult(BusinessAccountSchema, testData);
console.log('バリデーション分析結果:', analysis);
バンドルサイズ最適化とパフォーマンス
// 必要な機能のみインポート(tree shaking最適化)
import { string, number, parse } from 'valibot';
// ミニマルなスキーマ(最小バンドルサイズ)
const MinimalSchema = object({
id: string(),
count: number()
});
// 機能別インポートによる最適化
import {
object,
string,
email, // メール検証が必要な場合のみ
url, // URL検証が必要な場合のみ
minLength // 長さ検証が必要な場合のみ
} from 'valibot';
// 条件付きインポートパターン(動的インポート)
const createAdvancedSchema = async () => {
const { regex, transform, custom } = await import('valibot');
return object({
username: pipe(
string(),
transform(s => s.toLowerCase()),
regex(/^[a-z0-9_]+$/),
custom(s => s !== 'admin')
)
});
};
// パフォーマンス測定
function benchmarkValidation<T>(
schema: BaseSchema<any, T, any>,
data: unknown,
iterations: number = 1000
): { averageTime: number; successRate: number } {
let successCount = 0;
const startTime = performance.now();
for (let i = 0; i < iterations; i++) {
const result = safeParse(schema, data);
if (result.success) {
successCount++;
}
}
const endTime = performance.now();
const averageTime = (endTime - startTime) / iterations;
const successRate = successCount / iterations;
return { averageTime, successRate };
}
// バッチバリデーション(大量データ処理)
function validateBatch<T>(
schema: BaseSchema<any, T, any>,
dataArray: unknown[]
): {
valid: T[];
invalid: { data: unknown; issues: any[] }[];
summary: { total: number; validCount: number; invalidCount: number };
} {
const valid: T[] = [];
const invalid: { data: unknown; issues: any[] }[] = [];
for (const data of dataArray) {
const result = safeParse(schema, data);
if (result.success) {
valid.push(result.output);
} else {
invalid.push({ data, issues: result.issues });
}
}
return {
valid,
invalid,
summary: {
total: dataArray.length,
validCount: valid.length,
invalidCount: invalid.length
}
};
}
// メモ化によるパフォーマンス最適化
const schemaCache = new Map();
function getCachedSchema(schemaKey: string, schemaFactory: () => any) {
if (!schemaCache.has(schemaKey)) {
schemaCache.set(schemaKey, schemaFactory());
}
return schemaCache.get(schemaKey);
}
// 使用例
const userSchema = getCachedSchema('user', () =>
object({
name: string(),
email: pipe(string(), email())
})
);
// ストリーミングバリデーション(リアルタイム処理)
class StreamValidator<T> {
private schema: BaseSchema<any, T, any>;
private onValid: (data: T) => void;
private onInvalid: (data: unknown, issues: any[]) => void;
constructor(
schema: BaseSchema<any, T, any>,
handlers: {
onValid: (data: T) => void;
onInvalid: (data: unknown, issues: any[]) => void;
}
) {
this.schema = schema;
this.onValid = handlers.onValid;
this.onInvalid = handlers.onInvalid;
}
process(data: unknown): void {
const result = safeParse(this.schema, data);
if (result.success) {
this.onValid(result.output);
} else {
this.onInvalid(data, result.issues);
}
}
}
// ストリーミングバリデーターの使用
const userStreamValidator = new StreamValidator(
object({ name: string(), age: number() }),
{
onValid: (user) => console.log('有効なユーザー:', user),
onInvalid: (data, issues) => console.log('無効なデータ:', data, issues)
}
);
// リアルタイムデータ処理
userStreamValidator.process({ name: '田中', age: 30 }); // 有効
userStreamValidator.process({ name: '', age: 'invalid' }); // 無効