FORCIA CUBEフォルシアの情報を多面的に発信するブログ

express-validatorのSchema-Validationがよくわからないのでソースを読んでみる

2019.12.02

アドベントカレンダー2019 エンジニア

FORCIAアドベントカレンダー2019 2日目の記事です。

2016新卒入社の龍島です。最近業務でexpress-validatorのSchema-Validationを利用しているのですが、あまり丁寧な情報がなく、利用方法がわからない部分がありました。そこで前日の光山の記事に触発され、ソースを読んで調べてみました。少しでも同じ悩みを持っている方の役に立てれば幸いです。今困っている!使い方を早く知りたい!という方は「Schema-Validationの設定方法」の章を読んでいただければと思います。

express-validatorとは

公式ドキュメントの説明を借りると、

express-validator is a set of express.js middlewares that wraps validator.js validator and sanitizer functions.

Node.jsのwebアプリケーションフレームワークであるExpress上でミドルウェアとしてvalidator.jsの機能を利用して、フィールドをバリデート、サニタイズできるようにしたものです。

利用方法は下記のような感じです(公式ドキュメントより)。

const { check, validationResult } = require('express-validator');

app.post('/user', [
  // username must be an email
  check('username').isEmail(),
  // password must be at least 5 chars long
  check('password').isLength({ min: 5 })
], (req, res) => {
  // Finds the validation errors in this request and wraps them in an object with handy functions
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }

  User.create({
    username: req.body.username,
    password: req.body.password
  }).then(user => res.json(user));
});

ミドルウェアとしてcheck('${field名}').isEmail()などとするとフィールドがメールアドレスの形式かチェックできるといった次第ですね。

チェックはchainingが可能で、

check("password")
    .isLength({min: 5})
    .isInt()

とすれば長さが5以上で数字のみのバリデーションが可能です。

Schema-Validation

公式ドキュメントにもある通り、フィールド名をキーとしてオブジェクトでバリデーションの定義を記述できるものです。複数ページで共通のバリデーションを行う場合や、定義を動的に生成する際に便利そうですね。公式ドキュメントのサンプルを簡略化したものを載せておきます。

const { checkSchema } = require('express-validator');
app.put('/user/:id/password', checkSchema({
  id: {
    // The location of the field, can be one or more of body, cookies, headers, params or query.
    // If omitted, all request locations will be checked
    in: ['params', 'query'],
    errorMessage: 'ID is wrong',
    isInt: true,
    // Sanitizers can go here as well
    toInt: true
  },
  password: {
    isLength: {
      errorMessage: 'Password should be at least 7 chars long',
      // Multiple options would be expressed as an array
      options: { min: 7 }
    }
  },
  firstName: {
    isUppercase: {
      // To negate a validator
      negated: true,
    },
    rtrim: {
      // Options as an array
      options: [[" ", "-"]],
    },
  },
  // Wildcards/dots for nested fields work as well
  'addresses.*.postalCode': {
    // Make this field optional when undefined or null
    optional: { options: { nullable: true } },
    isPostalCode: true
  }
}), (req, res, next) => {
  // handle the request as usual
})

フィールド名をキーとすることや、inで対象のlocationを絞れること、errorMessageでエラーメッセージを定義することは読み取れますが、isIntはbooleanを設定しているのに対して、isLengthはオブジェクトでoptionsがあったり、rtrimoptionsが二重配列だったりと、どのように設定していけばよいのか読み取ることは難しいです。ドキュメントから読み取れなければソースを読みましょう。簡単にソースを読めるのはOSSのいいところです。

以下、isIntなどmethodに対する設定値(true, Object, Arrayなど)をソースに倣ってmethodCfgと呼ぶことにします。

ソースを読む

v6.2.0のソースを読んでいきます。checkSchemaはこのあたりです。40行程度しか無いのでサクッと読めそうですね。

全体をざっと見ると、各fieldに対してValidationChainを作って配列で返しているようです。

    const chain = check(field, ensureLocations(config, defaultLocations), config.errorMessage);

で作成したValidateChainオブジェクトに対して

        (chain[method] as any)(...options);

でoptionsを引数としてmethod(isIntなど)を実行して、chainingしています。つまり最初に例として出した

check("password")
    .isLength({min: 5})
    .isInt()

の形を作っているわけですね。

気になっているmethodCfgに何を入れればよいかという点は、下記のあたりを見るとわかります。

        let options: any[] = methodCfg === true ? [] : methodCfg.options || [];
        if (options != null && !Array.isArray(options)) {
          options = [options];
        }

optionsという変数に対してmethodCfgがtrueもしくはmethodCfg.optionsが無ければ空配列、methodCfg.optionsを代入します。またoptionsが配列でなかった場合は配列化していますね。このoptionsが先程のmethod実行時の引数にスプレッド演算子で展開されて渡され、n個目の要素が第n引数となります。

Schema-Validationの設定方法

ソースを読んだことでSchema-Validationの設定方法が見えてきました。

設定可能なmethod

check()にchainで繋げられるものがmethodとして利用可能なので、validatorjsのvalidatorssanitizersはもちろん、express-validator独自であるvalidationsanitizationのadditional-methodsもが利用可能です。

methodCfgの設定方法

各methodで、引数が必要なくinerrorMessageも指定しない場合はtrueを設定しておけば良いです。

id: {
    isInt: true
 }

引数が必要な場合はoptionsとして設定します。

password: {
    isLength: {
        options: { min: 7 }
    }
}
// => check('password').isLength({min: 7})

引数が複数ある場合は配列にして渡します。※matchesはmodifierも引数に取れます。

text: {
    matches: {
        options: ['abc', 'i']
    }
}
// => check('text').matches('abc', 'i')

引数に配列を渡す場合は注意が必要です(これにハマりました...)。

sort: {
    isIn: {
        options: [['recommend', 'lowPrice', 'highPrice']]
    }
}
// => check('sort').isIn(['recommend', 'lowPrice', 'highPrice'])

配列はスプレッド演算子で展開されてmethodに渡されるため、配列を渡したい場合は二重配列にする必要があります。

その他は公式ドキュメントのサンプルの通りinでfieldの場所を指定できたり、errorMessageでバリデートエラー時のエラーメッセージを設定できたりします。

さいごに

公式ドキュメントのサンプルに大体のことは書いてありますが、やはり不明な部分のソースを読んでみるとかなり理解が深まりますね。今回の該当部分以外にもValidationChainの仕組みなどを読んでみましたが、参考になる部分も多かったです。

時間を見つけて今後はExpress本体やvalidator.jsの実装などをソースコードリーディングしようと思います。

この記事を書いた人

龍島 広人

旅行プラットフォーム事業部 エンジニア 2016年新卒入社。
DevOpsや仮想化技術をメインにしていたが、最近はTypeScriptの型システムに
非常に興味があり、型に守られた安全な開発を目指して日々精進している。