インターラプト開発者ブログ

インターラプトのプロダクト・サービスに関する開発ブログ

ajvで楽々バリデーション

前提

  • この記事ではTypeScriptを使用しています。

Webエンジニアの篠田(@syuilo)です。インターラプトでは現在主にNode.jsを使ったバックエンド開発を担当しています。

ところで皆さん、バリデーション、していますか?

Web APIを作る上では、どんなデータがネットワークの向こうから飛んでくるか分からないという性質上、リクエストパラメータのバリデーションが多かれ少なかれ必要になるのは言うまでもありません。

具体的なバリデーションの方法は色々あります。シンプルなものなら愚直に受け取ったパラメータそれぞれについて言語標準の方法(JSならtypeofなど)で検証する手も考えられますが、実際のプロダクトにおいてはパラメータの数や型は多岐に渡ります。そのような場合、何らかのライブラリを使うのが現実的です。

最も良い方法は、フレームワーク標準のバリデータを使うことです。高機能なフレームワークであればまずリクエストパラメータのバリデーション機能は付いています。

しかし軽量なフレームワークだとそのような機能はオミットされていることが多いので、そういった場合に使えるバリデーションライブラリをナレッジのアウトプットも兼ねて紹介したいと思います。


この記事で紹介するのはajvというTypeScript製のライブラリです。Node.jsの高機能フレームワーク、Nest.jsでも採用されているものです。 ajvを使う前は、自作のバリデーションライブラリを使っていました。しかし型の表現力などで限界を感じてきたので、今回ajvに乗り換えることにしました。

ajvの最大の特徴は、JSON Schemaに基づいてバリデーションを行える点です。それが他の様々なライブラリの中からajvを選択した理由の一つでもあります。JSON Schemaでバリデーションを行えると何が嬉しいのかというと、移植性が高くパラメータ定義をそのままSwaggerなどのAPI定義に流用できること、スキーマから型を生成して使いまわせること、広く使われている形式なので導入コスト・開発コストを下げられることです。 他のライブラリだと、より短く簡潔にバリデータを定義できるようになっていたりしますが、それはそのライブラリ限定の使い方になるため再利用が低いと思っています。

他にもajvの特徴として、 - バリデーションライブラリの中ではかなりメジャーな方 - 前述したように、NestやFastifyといった大手フレームワークでも採用されている - ネイティブTypeScriptで書かれているため型との相性がよい - バリデーション関数を事前にコンパイルでき、パフォーマンスが高い

などがあります。

それでは実際にajvを使ったバリデーションの例を紹介します。 なお、この記事ではJSON Schemaについての詳細は省き、ajvの使い方の部分のみ説明します。

インストール

npm i ajv

ネイティブTypeScriptで書かれているため、別途型定義をインストールする必要はありません。🉐

バリデータの定義とコンパイル

import Ajv from 'ajv';
const ajv = new Ajv();

// スキーマ定義
const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string' },
  },
  required: ['foo'],
  additionalProperties: false,
};

// スキーマ定義からバリデータ関数をコンパイル
const validate = ajv.compile(schema);

前述した通りバリデータの定義はJSON Schemaで行うので、JSON Schemaの知識さえあれば簡単に使えるようになっています。

バリデーション

const data = {
  foo: 1,
  bar: 'abc',
};

const valid = validate(data);
if (!valid) console.log(validate.errors);

生成されたバリデータ関数は使い回せます。バリデーションが失敗すると、バリデータ関数のerrosプロパティにエラー詳細が入ります。エラー情報には、バリデーションが失敗した具体的なプロパティ名などが含まれ、ユーザーに分かりやすいエラーレスポンスを返すのに役立てることができます。

具体的にはこのようなオブジェクトになっています:

instancePath: "/foo"
schemaPath: "#/properties/foo/type"
keyword: "type"
params: Object {type: "string"}
message: "must be string"

ajv.errorsText関数を使うと、このエラーオブジェクトをもとにわかりやすいメッセージを生成することも可能です。

 ajv.errorsText(validate.errors) // => "Invalid: data/foo must be string, data/bar must be <= 3"

デフォルト値について

大抵のAPIでは、パラメータが省略された際にデフォルトの値を設定することが一般的です。ajvにもデフォルト値をサポートする機能があります。

まず、ajvのオプションでuseDefaultsを有効にします:

const ajv = new Ajv({
    useDefaults: true,
});

次に、スキーマ定義でdefaultキーを各プロパティ定義に含めます。

const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', default: 'hello' },
  },
  required: ['foo'],
  additionalProperties: false,
};

const validate = ajv.compile(schema);

このようにバリデータを定義した上で、バリデーション関数に値を渡すとバリデーションと同時に渡された値を直接ミューテートする形でデフォルト値の設定が行われます。:

const data = {
  foo: 1,
};

const valid = validate(data);
if (!valid) console.log(validate.errors);

console.log(data); // => { foo: 1, bar: 'hello' }

ajvはrunkitで動作確認することができます。ぜひ試してみてください。

また、ajvではJSON Schemaの他にも JTD(JSON Type Definition) という形式でスキーマを定義することもできますが、一長一短あり、個人的には短(定義できるのはあくまでも型のみであり、文字列のフォーマットや文字数などといった型で表せない制限を行うことはできない、等)の方が上回っていると感じたため実際のプロダクトでは使っていません。 興味がある方はJTDも試してみてください。