はじめまして、Jodaiです。フロントエンドを中心に開発を行っているアプリケーションエンジニアです(自称)。
比較的メジャーなJS/TS向けのバリデーションライブラリ3つ(zod、yup、valibot)を実務で使用した経験から、2025年2月時点での状況を「書きやすさ」と「今後の動向」を観点に比較し、メモとして残します。
Githubのスター数比較
最古参のyupは2023年頃までトップを独走していましたが、2020年にzodがリリースされてから急速に成長し、2022年から2023年にかけてzodにトップの座を譲り渡しています。
新参のvalibotは2023年後半にリリースされ、まだzodやyupと並ぶほどではありませんが、かなりの勢いがある印象です。
npmのDL数比較
こちらもGitHubのスター数と同様の傾向が見られます。
概要とそれぞれのメリット・デメリット
yup
初回リリース : 2016/6/25
最新版リリース : 2023/2/8(v1.0.0)
Yup is a schema builder for runtime value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformation.
Yupは、実行時の値の解析と検証のためのスキーマ・ビルダーである。スキーマを定義する、値を変換してマッチさせる、既存の値の形状をアサートする、あるいはその両方を行う。Yupのスキーマは非常に表現力が豊かで、複雑で相互依存的な検証や値の変換をモデル化することができる。
この手のライブラリのパイオニア的存在であり、弊社でも多くのプロジェクトに採用されている印象があります。
メリット
- クロスフィールド(同一スキーマ内の別の値)の参照が容易
- カスタムルールをメソッドチェーンに直接追加可能
- カスタムメッセージの設定が簡単
デメリット
- 型推論がやや弱い
unionリテラル型やbigintなどに対応しておらず、型推論するとany
として扱われてしまう。
zod
初回リリース : 2020/4/5
最新版リリース : 2025/2/12(v3.24.2)
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple
string
to a complex nested object.
ZodはTypeScriptファーストのスキーマ宣言・検証ライブラリです。「スキーマ」という用語は、単純な文字列から複雑なネストされたオブジェクトまで、あらゆるデータ型を広く指すために使用されています。
メリット
- 型推論がyupより優秀
- 基本的な記法がyupとほぼ同じため、yupからの移行が容易
デメリット
- カスタムルールやカスタムメッセージの定義がyupに比べて複雑
valibot
初回リリース : 2023/7/13
最新版リリース : 2025/2/9(v1.0.0-rc.0)
Hello, I am Valibot and I would like to help you validate data easily using a schema. No matter if it is incoming data on a server, a form or even configuration files. I have no dependencies and can run in any JavaScript environment.
Valibotは、スキーマを使って簡単にデータを検証するお手伝いをします。サーバ上の受信データ、フォーム、設定ファイルなど、どのようなデータでも対応可能です。依存関係がなく、あらゆるJavaScript環境で動作します。
メリット
- zodに比べて90%以上バンドルサイズが小さく、非常に軽量
- 型推論の強さはzodと同等
- メソッドチェーンではなく、パイプライン形式のため、カスタムルールの定義が簡単(importしたメソッドをそのままパイプラインに追加可能)
- カスタムメッセージの設定もzodに比べて簡単
デメリット
- yupやzodとは異なる記法が採用されているため、移行がやや面倒
- 後発のため、情報が比較的少ない
通常の記法
yup
import * as yup from 'yup';
const schema = yup.object({
id: yup.number().required(),
title: yup.string().min(1).max(50).required(),
});
type InputType = yup.InferType<typeof schema>;
可読性が高く、zodにもほぼ同様の特徴が引き継がれています。
zod
import { z } from 'zod';
const schema = z.object({
id: z.number().min(0).nonempty(),
title: z.string().min(1).max(50).nonempty(),
});
type InputType = z.infer<typeof schema>;
yupとほぼ同じ記法です。
valibot
import * as v from 'valibot';
const schema = v.object({
id: v.pipe(v.number(), v.minValue(0), v.nonEmpty()),
title: v.pipe(v.string(), v.minLength(1), v.maxLength(50), v.nonEmpty()),
});
type InputType = v.InferOutput<typeof schema>;
valibotは記法が少し異なり、メソッドチェーンではなくパイプライン形式で複数の条件を組み合わせる仕組みが特徴です。
記法が異なるため、前者2つからの移行はやや面倒です。
型推論
3つとも現状型推論に対応しています。
ただし、yupについては一部のメソッドがany
として扱われてしまうため、やや劣ります。
クロスフィールドのバリデーション
パスワードチェックなど、クロスフィールドのバリデーションはよく使用されるため、それぞれ比較していきます。
yup
import * as yup from 'yup';
const schema = yup.object({
password: yup.string().required(),
password_confirm: yup.string()
.oneOf([yup.ref('password')])
.required(),
});
同一オブジェクト内の値を簡単に参照できるため、非常にシンプルです。
zod
import { z } from "zod";
const schema = z.object({
password: z.string().nonempty(),
password_confirm: z.string().nonempty(),
}).superRefine(({ password, password_confirm }, ctx) => {
if (password !== password_confirm) {
ctx.addIssue({
path: ['password_confirm'],
code: z.ZodIssueCode.custom,
message: 'パスワードが一致しません',
});
}
});
superRefine
をスキーマオブジェクト全体に対して設定する形式です。
ただし、オブジェクト内部のバリデーションを先に通過しないとsuperRefine
に到達しないため、メッセージが表示されるタイミングが遅れることがあります。
valibot
import * as v from 'valibot';
const schema = v.pipe(
v.object({
password: v.pipe(v.string(), v.nonEmpty()),
password_confirm: v.pipe(v.string(), v.nonEmpty()),
}),
v.forward(
v.partialCheck(
[['password'], ['password_confirm']],
(input) => input.password === input.password_confirm,
'パスワードが一致しません'
)
)
);
前者2つと異なりpipe
でつなぐ形式です。
zodと同様に、オブジェクト内部のバリデーションを先に通過しないとforward
の判定に進まないため、メッセージが表示されるタイミングが遅れることがあります。
カスタムルールの定義
実際の使用ではカスタムルールを定義することが多いため、それぞれのライブラリでの実装方法を比較していきます。
yup
import { addMethod, Message, string, StringSchema } from 'yup';
import { checkKatakana } from '/regex';
// 定義
addMethod<StringSchema>(
string,
"katakana",
(message: Message = 'カタカナで入力してください') => {
return this.test("katakana", (value, testContext) => {
if (checkKatakana.test(value)) {
return true;
}
return testContext.createError({ message });
});
}
);
declare module "yup" {
interface StringSchema<TType, TContext, TDefault, TFlags> {
katakana(): this;
}
}
// 使用
yup.string().katakana();
yupではaddMethod
を使用して既存のメソッドチェーンにカスタムルールを追加できます。また、declare module
を使用することでエディタ側で補完が効くようになります。
zod
import { z } from "zod";
import { checkKatakana } from '/regex';
// 定義
const katakana = ({ message = 'カタカナで入力してください' }: { message?: string }) => {
return (value: string, ctx: z.RefinementCtx) => {
if (!checkKatakana.test(value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message,
});
}
};
};
// 使用
z.string().superRefine(katakana({}));
zodではメソッドチェーンに直接カスタムルールを追加することはできません。代わりに、refine
やsuperRefine
を使用して作成した関数を引数として渡すことで適用できます。
valibot
import * as v from 'valibot';
import { checkKatakana } from '/regex';
// 定義
const katakana = (message = 'カタカナで入力してください') => v.regex(checkKatakana, message);
// 使用
v.pipe(
v.string(),
katakana(),
);
定義した関数をパイプラインに追加するだけで済むため、個人的には最も簡単に感じます。
カスタムメッセージの設定
yup
import * as yup from 'yup';
yup.setLocale({
string: {
min: ({ min }) => `${min}文字以上入力してください`,
max: ({ max }) => `${max}文字以内で入力してください`,
},
});
setLocale
を使用することで、簡単にカスタムメッセージを設定できます。
zod
import { z } from "zod";
z.setErrorMap((issue, ctx) => {
switch (issue.code) {
case z.ZodIssueCode.too_small:
if (issue.type === 'string') {
return { message: `${issue.minimum}文字以上入力してください` };
}
case z.ZodIssueCode.too_big:
if (issue.type === 'string') {
return { message: `${issue.maximum}文字以内で入力してください` };
}
}
return { message: ctx.defaultError };
});
issue
の内容に応じてswitch
文で分岐し、カスタムメッセージを設定する形式です。
可読性が低く、バグが発生しやすい印象を受けました。
valibot
import * as v from 'valibot';
const minLength = (min: number) => {
return v.minLength(min, `${min}文字以上入力してください`);
};
const maxLength = (max: number) => {
return v.maxLength(max, `${max}文字以内で入力してください`);
};
const schema = v.pipe(
v.string(),
minLength(1),
maxLength(50)
);
作成したメソッドをpipe
に追加するだけで済むため、直感的でわかりやすいです。
今後の動向
yup
最終アップデートが2023年で、特に今後のロードマップも見つからないため、あまり動きがない印象です。そのため、他のライブラリへの移行を検討した方が良いかもしれません。
zod
GitHubのスター数を見ると、現在は覇権を握っており、開発も活発に行われています。ただし、具体的なロードマップは見つかりませんでした。
valibot
積極的に開発が行われている印象で、ロードマップも公式ブログで提示されています。今後もさらに便利になっていくことが期待されます。
結論
yupについては完全にレガシーになってしまった印象を持ったため、今後の技術選定では選択肢に上がらないと考えています。
zodについては、コードがやや複雑になる印象がありますが、開発が活発に行われているため、今後のアップデートで大きく変わる可能性があります。
また、情報が多いことから、大規模なチーム開発ではvalibotよりも優位性が高いと感じました。
valibotについては、まだ新しいためzodに比べて情報が少ないですが、シンプルな記法やカスタマイズ性、軽量さが特徴で、中小規模の開発ではベストプラクティスになるのではないかと考えています。
さらに、開発が活発なため、今後は覇権を握る可能性もある印象です。
今回紹介した3つ以外にも、勢いのある競合(Typia、TypeBox、ArkType)が存在するため、競争が進み徐々にzodの牙城が崩れていく可能性があります。
また、HonoをはじめとしたRPC対応フレームワークも勢いづいているため、これらのフレームワークとの親和性が重要になってくるでしょう。
なおご指摘等ありましたらコメントいただけますと幸いです。