JavaScript における Schema Validation ツール Yup を試してみた。

こんにちは k-jun です。今回は JavaScript における Schema Validation ツール Yup を使ってみます。

https://github.com/jquense/yup

パッとみてわかることは以下ぐらいでしょうか。

  • Api としては Joi に影響を受けている。
  • Parse と Validation が別々の処理として定義されている。

まあ、最もよく理解するには使ってみることだと思うので、使っていきます。

npm install yup

schema というように、あるオブジェクトの定義として schema を作成すると、このデータ型に Cast してくれたり Validation が行える様子。 ひとまず作って見ます。

import yup from "yup"

async function main() {
  let schema = yup.object().shape({
    title: yup.string().required(),
    body: yup.string().required(),
  });

  // schema.fields.title = yup.string().optional()
  const invalid = { body: "test body" }
  schema.validate(invalid).catch(err => console.error(err))
}

main()
$ node index.js
ValidationError: title is a required field
    at createError (/Users/keijun.kumagai/source-code/playground-javascript/node_modules/yup/lib/util/createValidation.js:54:21)
    at /Users/keijun.kumagai/source-code/playground-javascript/node_modules/yup/lib/util/createValidation.js:72:107 {
  value: { body: 'test body' },
  path: 'title',
  type: 'required',
  errors: [ 'title is a required field' ],
  inner: [],
  params: {
    value: undefined,
    originalValue: undefined,
    label: undefined,
    path: 'title'
  }
}

おお、大丈夫そう。"Schema objects are immutable, so each call of a method returns a new schema object" と書いてあるので、schema を変更することも試してみます。

import yup from "yup"

async function main() {
  const schema = yup.object().shape({
    title: yup.string().required(),
    body: yup.string().required(),
  });

  schema.fields.title = yup.string().optional()
  const invalid = { body: "test body" }
  schema.validate(invalid).catch(err => console.error(err))
}

main()
$ node index.js

通ってしまった...。意味を取り違えたのだろうか...。これで良いのだろうか...。

気を取り直して、他の実験もしていきます。まずは Cast を見てみる。

async function main() {
  const schema = yup.object().shape({
    title: yup.string().required(),
    body: yup.string().required(),
    due: yup.date(),
    done: yup.boolean(),
    priority: yup.number(),
  });

  const test = {
    title: "test todo",
    body: "",
    due: "2021-08-27",
    done: "0",
    priority: "10",
  }

  const x = schema.cast(test)
  console.log(x)
}

main()
$ node index.js
{
  priority: 10,
  done: false,
  due: 2021-08-26T15:00:00.000Z,
  body: '',
  title: 'test todo'
}

おお、Boolean の変換、Date の変換、Integer の変換なども諸々やってくれるようです。ORM のオブジェクトスキーマとして使用できる気もしてきました。

他、様々な便利メソッドが用意されているようです。以下ざっと見てみた感じ特に便利そうなものを紹介していきます。

ref

オブジェクトの他の箇所を参照として定義することが出来ます。オプション値を設定するなど、何かと便利そうです。

import { object, ref, string } from "yup"

async function main() {
  let schema = object({
    baz: ref('foo.bar'),
    foo: object({
      bar: string(),
    }),
    x: ref('$x'),
  });

  console.log(schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } }));
}

main()
$ node index.js
{ x: 5, foo: { bar: 'boom' }, baz: 'boom' }

concat

2つの schema の定義を結合します。上手く使えば、柔軟な型の代わりのように動作するかもしれません。

import { object, ref, string, mixed } from "yup"

async function main() {

  let schema = mixed();
  schema = schema.concat(object().shape({
    title: string().required(),
  }))
  schema = schema.concat(object().shape({
    body: string().required(),
  }))

  schema.validate({}).catch(err => console.error(err))

}

main()

と、ここまで見ていておもいましたが、これ TypeScript で良くないですか...? 他の関数をみても 型の結合、requried、non-required の設定。 default 値、型の和集合などと 全部 TypeScript の型の柔軟性に飲み込まれる気がします。

最終的に TypeScript でしっかりと型管理をすれば、問題ないのでは。それ以外に価値を自分はこの Yup に見いだせませんでした。

それでは今日はこのへんで。