Ajvを使ってJSONのバリデーションをする（JavaScript/TypeScript）

# 環境

- [ajv 6.12.2](https://github.com/ajv-validator/ajv)
- TypeScript 3.8.3 (on NodeJS)

# はじめに

Ajv はブラウザ環境でも使用できますが、ここでは NodeJS（TypeScript）で使った場合の例になります。
[githubページ](https://github.com/ajv-validator/ajv) に必要十分な情報はありますが、分量が多いので、ここでは主に**スキーマ記述例**をまとめています。

ブラウザで使う場合の参照先は、[こちら（github）](https://github.com/ajv-validator/ajv#using-in-browser)。

# Ajv の特徴

- [JSON Schema draft-07](http://json-schema.org/draft/2019-09/json-schema-validation.html) に準拠している。
  - 以下のようにして、draft-06 を利用することも可能です（[参照先](https://github.com/ajv-validator/ajv#using-version-6)）。 

 ```typescript
 ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
 ```
- 他の同種のライブラリより高速（[参照先](https://github.com/ajv-validator/ajv#performance)）。
- [ajv-18n](https://github.com/ajv-validator/ajv-i18n) を使うと、エラーメッセージの多言語化も対応可能です。

# 使い方

```typescript
import Ajv from 'ajv'
// ここで自分たちの schema ファイルを読み込みます。
import schema from 'somewhere/schema.json'

const ajv = new Ajv()
const validate = ajv.compile(schema)

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

# schema ファイルの例

JSON ではコメントは使えませんが、ここでは説明のために //〜 としてコメントを追加して、且つ改行を入れています。 

```json
{
  // この値は他のschemaファイルから参照される時に使われるだけなので、
  // 実際にこのURLにschemaファイルを置いてダウンロード出来るようにする必要はありません。
  "$id": "http://example.com/schemas/schema.json",

  // この設定がない場合、draft 6 meta-schema に対してバリデーションが行われます。
  "$schema": "http://json-schema.org/draft-07/schema",

  "title": "Your title comes here",
  "description": "Your description comes here",
  // schema ファイルは、基本的に object になると思われるので、
  // トップレベルの type は object とします。
  "type": "object",

  // これにより、定義していないフィールドがあった場合にエラーとします。
  // 尚、定義されたレイヤーでしか有効ではありません。
  // 階層化された下位層の object に対しては、それ毎に定義が必要です。
  // 基本的に、"type": "object" の後にはつけたほうがいいでしょう。 
  "additionalProperties": false,

  // definitions に定義した値は、$ref としてスキーマ内から参照可能です(後述)。
  "definitions": {
    "languageStrings": {
      "description": "This is an example of definition",
      "type": "object",
      "minProperties": 1,
      "examples": [
        {
          "en": "English title",
          "ja": "日本語タイトル"
        }
      ],
    }
  },

  // properties にフィールドの定義をしていきます。
  "properties": {
    "$schema": {
      "type": "string"
    },
    "stringExample": {
      "description": "This is an example of string type",
      "type": "string"
    },
    "stringOfDateFormatExample": {
      "description": "This is an example of string type of the date format",
      "type": "string"
      "format": "date",
      "maxLength": 10,
      "examples": ["2020-01-23"]
    },
    "stringOfPatternExample": {
      "description": "This is an example of string type of the pattern",
      "type": "string",
      "pattern": "^0x[!-~]{40}$",
      "maxLength": 42
    },
    "numberExample": {
      "description": "This is an example of number type",
      "type": "number"
    },
    "arrayExample": {
      "description": "This is an example of array type",
      "type": "array",
      "items": {
        "type": "string"
      },
      "uniqueItems": true
    },
    "enumExample": {
      "description": "This is an example of enum",
      "enum": [
        "one",
        "two",
        "three"
      ]
    },
    "refExample": {
      "description": "This is an example of $ref",
      "$ref": "#/definitions/languageStrings",
      "examples": [
        {
          "en": "English"
        }
      ]
    },
    "objectWithAnyOfExample": {
      "description": "This is an example of object with anyOf",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "from": {
          "type": "string",
          "format": "date",
          "maxLength": 10,
          "examples": ["2020-01-23"]
        },
        "to": {
          "type": "string",
          "format": "date",
          "maxLength": 10,
          "examples": ["2020-03-21"]
        }
      },
      "anyOf": [
        {
          "required": [
            "from",
            "to"
          ]
        },
        {
          "required": [
            "from"
          ]
        },
        {
          "required": [
            "to"
          ]
        }
      ]
    },
    "dependenciesExample": {
      "description": "This is an example of object, dependencies and required",
      "type": "object",
      // 前述の通り、トップの階層で設定していても、ここでも "additionalProperties": false としなければ、
      // 意図しないフィールドが設定できてしまいます。
      "additionalProperties": false,
      // この定義により、xx を定義した時に、必ず yy を定義する、というルールを付けることが出来ます。
      // ここの例では width、height、depth のいずれかが定義された場合は、unit を定義しなければエラーになります。
      "dependencies": { "width": ["unit"], "height": ["unit"], "depth": ["unit"] },
      // ここに定義されたものは必須項目となります。
      "required": [
        "note"
      ],
      "properties": {
        "width": {
          "type": "number"
        },
        "height": {
          "type": "number"
        },
        "depth": {
          "type": "number"
        },
        "unit": {
          "enum": [
            "mm",
            "cm",
            "m"
          ]
        },
        "description": {
          "$ref": "#/definitions/languageStrings"
        },
        "note": {
          "$ref": "#/definitions/languageStrings"
        }
      }
    },
  },
  // ここに定義されたものは必須項目となります。
  "required": [
    "numberExample",
    "arrayExample"
  ],
  "minProperties": 0
}
```

# バリデーションルールについて

上記以外にも様々なバリデーションルールが設定可能です。
参照先：
https://ajv.js.org/json-schema.html
https://ajv.js.org/json-type-definition.html

# セキュリティ対応

https://github.com/ajv-validator/ajv#security-considerations にリストアップされている内容に準拠しているかを、以下のように Jest などで確認しておきましょう。

```typescript
import Ajv from 'ajv'
import schema from 'somewhere/schema.json'

// see: https://github.com/ajv-validator/ajv#security-considerations
describe('isSchemaSecure', () => {
  const ajv = new Ajv()
  const isSchemaSecure = ajv.compile(
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    require('ajv/lib/refs/json-schema-secure.json')
  )
  expect(isSchemaSecure(schema)).toBe(true)
})
```

例えば、以下の通り `format` や `pattern` を指定した時は、`maxLength` を指定しないとチェックに引っかかります。

```typescript
const isSchemaSecure = ajv.compile(require('ajv/lib/refs/json-schema-secure.json'));

const schema1 = {format: 'email'};
isSchemaSecure(schema1); // false

const schema2 = {format: 'email', maxLength: MAX_LENGTH};
isSchemaSecure(schema2); // true
```

# ブラウザで確認

https://www.jsonschemavalidator.net/ に schema と JSON を入れれば確認できます。

# CLI で確認

[ajv-cli](https://www.npmjs.com/package/ajv-cli) を使って、CLI でチェックすることも出来ます。