速習 Valibot
はじめに
フクロウラボでは一部のプロダクトでサーバーサイドの言語に TypeScript を採用しています。クライアントサイドからのリクエストや、外部マイクロサービスとのやり取りなどの、サービスの外の世界との接点になる部分では Zod を用いてスキーマによるバリデーションを行っています。
Zod はとても良いライブラリですが、筆者は代替ライブラリである Valibot を好んでおり直近の個人開発では Valibot を第一の選択肢として考えるようにしています。そこで今回は、チームメンバーへの布教も兼ねて Valibot について簡単に紹介したいと思います。
Valibot とは
Valibot は定義したスキーマを用いて値のバリデーションを行うライブラリです。代替のライブラリとしては前述の Zod や、Yup などが挙げられます。
記事執筆時点で GitHub のスター数は 7.3 K、main ブランチへの commit はほぼ毎日行われており、勢いがあり活発なオープンソースプロジェクトと言えるでしょう。
Valibot の魅力
Valibot の1番の魅力はバンドルサイズの小ささです。各機能が個別に export されているため、未使用コードはバンドル時にツリーシェイキングによって取り除かれます。
バンドル後のコードサイズは軽量であればあるほど嬉しいです。Cloudflare Workers、Cloudflare Pages などのアップロードできるサイズに制限のある実行環境においては軽量であることは大きな利点です。継続的デリバリーを迅速に実行することの助けにもなります。
コードの書き振りの観点だと Zod がオブジェクトのメソッドチェーンでスキーマを定義するのに対し、Valibot は関数をネストさせてパイプラインを構築することによりスキーマを定義します。スキーマで表現される制約が一目でわかりやすいのは Valibot の採用しているアプローチであると感じています。
Valibot の概念
Valibot を利用する上で抑えておきたい下記3つの概念について順に説明します。
- Schemas
- Actions
- Methods
Schemas
Schemas は値が特定の型に属していることをバリデーションするための概念です。
文字列型であることをバリデーションする string()
や、真偽値型であることをバリデーションする boolean()
などが含まれます。
import * as v from 'valibot';
const nameSchema = v.string();
const flagSchema = v.boolean();
上記で紹介したプリミティブの型だけでなく、一部の class や複雑な型も Schemas として提供されています。
import * as v from 'valibot';
const dateSchema = v.date();
const literalSchema = v.literal("literal_string");
const errorSchema = v.instanceOf(Error);
Methods
Methods は Schemas を操作するための概念です。
Schemas を用いてバリデーションを実行する parse()
safeParse()
や、値とマッチしていることを判定する is
などが含まれます。Methods の関数は Schemas を引数に受け取ります。
import * as v from 'valibot';
const nameSchema = v.string();
const errorSchema = v.instanceOf(Error);
v.parse(nameSchema, 'Alice');
v.is(errorSchema, new Error('error!'));
Valibot を使用する上での必須機能である pipe
も Methods にあたります。
import * as v from "valibot";
const passwordSchema = v.pipe(v.string(), v.nonEmpty(), v.minLength(8));
Actions
Actions は特定のデータ型に対して追加の制約や、値の変換ルールを設けるための概念です。
Methods のコード例で使用していた nonEmpty()
minLength()
は制約の追加を示すコードであり、これらは Actions に該当します。
これまでに紹介した Schemas、Methods と組み合わせて使用することで、より実用的でリッチなバリデーションを実現できます。例えば、Methods である pipe()
の引数に Schemas と Actions を一緒に渡すことで特定のデータ型の値であることを担保した上で細かい制約を設けることができます。
import * as v from "valibot";
const uuuidSchema = v.pipe(v.string(), v.uuid());
const emailSchema = v.pipe(v.string(), v.email());
利用例
Valibot が必要になるケースは Zod などの他のスキーマバリデーションライブラリが必要なケースと一致するかと思います。そのためそれらを利用していたケースで、Valibot を代替ライブラリとして採用することが可能であると考えて問題ないでしょう。
今回は HTTP レスポンスボディのバリデーションでの利用例を紹介します。
HTTP レスポンスボディのバリデーション
fetch
利用時の HTTP レスポンスでは JSON を取得できます。しかし値は unknown として型付けられています。バリデーションを実行し、後続の処理では期待した型の値として扱いたいです。
下記のような実装により実現することが可能です。
const responseSchema = v.object({
id: v.number(),
name: v.string(),
createdAt: v.pipe(v.string(), v.transform(Date)),
});
const getData = async () => {
const response = await fetch("https://example.com/");
const json = await response.json();
const result = v.parse(responseSchema, json);
return result;
};
Schemas を定義しておき、parse
の引数に Schemas と対象の値を渡すことによりバリデーションが実行されています。parse
の戻り値の型は Schemas から推論された型となっています。今回だと変数 result は下記のように推論されています。
const result: {
createdAt: string;
id: number;
name: string;
}
parse
はバリデーションエラーになった際に throw
するため、エラー時に特定の処理を行いたいような場合は catch
してハンドリングする必要があります。
const getData = async () => {
...
try {
const result = v.parse(responseSchema, json);
return result;
} catch (error) {
console.log("Error parsing response!");
}
};
parse
の代わりに safeParse
を用いることでエラーの有無を boolean
で評価してハンドリングすることも可能です。個人的には戻り値をハンドリングするコードの方が見通しが良いと感じるため safeParse
を使うことが多いです。
const getData = async () => {
...
const { success, output } = v.safeParse(responseSchema, json);
if (!success) {
console.log("Error parsing response!");
return;
}
return output;
};
Zod からの移行
ここまで読んだ方の中には Zod からの移行を検討したくなった方もいるのではないでしょうか。
Valibot のドキュメントには移行の手引きが記載されています。両者は提供されている機能が類似しており、移行は容易であるとのことです。(※筆者は Zod からの移行を実施した経験はありません)
さいごに
TypeScript のスキーマバリデーションライブラリである Valibot を紹介しました。
他の代替ライブラリと比べて画期的な機能が搭載されていたりはしないですが、バンドルサイズが小さく軽量であるという非常に大きな利点を持っています。
バリデーションライブラリ導入の際には Valibot を検討してみてはいかがでしょうか。
Discussion