JSON Schema 中心設計 - FlowType から RAML まで -


# はじめに

Web サービスの運用を続けていくと，依存関係が徐々に複雑になっていきます．そしてメンテナンスするものが増えた結果，それらが相互に乖離していく，といったことが起こりがちです．

そこで今回は，JSON Schema のみをメンテナンスしていくことで，動的チェック (バリデーション)，静的チェック (FlowType)，API ドキュメント生成，スタブ作成といった様々な恩恵を享受し，品質と保守性を同時に向上させるアプローチについて書いていきます．この JSON Schema を中心に据えたエコシステムを，__JSON Schema 中心設計__と呼ぶことにします．

JSON Schema の仕様については割愛しますので，必要な方は [こちら](http://json-schema.org/documentation.html) をご覧下さい．また，本記事では JavaScript での事例を紹介しますが，他の言語でも同様の適用ができるかと思います．

# アプローチ

本記事では，以下の user スキーマを利用します．可読性が高く，かつ `#` でコメントを付与できるため，以下のように json ではなく yaml で書くのがおすすめです．

```yaml:user.yaml
---
  $schema: http://json-schema.org/draft-04/schema#
  id: user
  type: object
  required:
    - id
    - lastName
    - firstName
    - state
  properties:
    id:
      description: user id
      type: number
    lastName:
      description: user's last name
      type: string
    firstName:
      description: user's first name
      type: string
    state:
      description: user state
      type: number
      enum:
        - 1  # active
        - 2  # inactive
  additionalProperties: false
```

## 1. 動的チェック (バリデーション)

JSON Schema で1番よく利用されるのは，この動的チェックかと思います．対象データが想定しているフォーマットかどうかをシンプルに検証することができ，不正なリクエストや，API の仕様変更などに簡単に気づくことができます．また，`minimum`，`maximum` といった単純なものから正規表現を扱える `patterns` といったものまで，バリデーション用に様々なプロパティが定義されており，必要によっては Strict なバリデーションも可能です．

JSON Schema でバリデーションをかけるライブラリはいくつかありますが，以下では [is-my-json-valid](https://github.com/mafintosh/is-my-json-valid) というライブラリを用いています．

```js
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
import isMyJsonValid from 'is-my-json-valid'
import assert from 'assert'

// Schema ファイルの読み込み
const schemaFilePath = path.resolve(__dirname, 'user.yaml')
const schema = yaml.safeLoad(
  fs.readFileSync(schemaFilePath, 'utf8'),
  { schema: yaml.JSON_SCHEMA }
)

const userData = { id: 1, lastName: 'Yamada', firstName: 'Taro', state: 1 }

// データが想定している Format かどうか検証
const validator = isMyJsonValid(schema)
assert(validator(userData), validator.errors)

// その後の処理
// ...
```

最近は Isomorphic な JS 実装をよく見かけますが，上記のコードをサーバ/クライアントの両サイドから読み込むことで，例えばフロントでのフォームバリデーションと，サーバでのリクエストボディのダブルチェックを，それらが乖離することなくアップデートしていくことができます．当然ですがメンテナンスするものは JSON Schema のみです．

## 2. 静的チェック (FlowType)

動的チェックだけでなく，型アノテーションによる静的チェックもしたい場合，[json-schema-to-flow-type](https://www.npmjs.com/package/json-schema-to-flow-type) を用いることで，JSON Schema から FlowType を自動生成することができます．

```js
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
import { parseSchema } from 'json-schema-to-flow-type'

// Schema ファイルの読み込み
const schemaFilePath = path.resolve(__dirname, 'user.yaml')
const schema = yaml.safeLoad(
  fs.readFileSync(schemaFilePath, 'utf8'),
  { schema: yaml.JSON_SCHEMA }
)

console.log(`/* @flow */\n\n${parseSchema(schema)}`)
```

以下のような FlowType が出力されます．リクエストボディなどのスキーマ定義を JSON Schema に記述し，メンテナンスしていくことで，自動的に FlowType もアップデートされていき，型の恩恵を授かれます．これにより，自分は FlowType の型定義を書くことは最近はほとんどなく，同じようなオブジェクトの定義を二重管理することがなくなりました．

```js
/* @flow */

export type User = {
  id: number;
  lastName: string;
  firstName: string;
  state: 1 | 2;
};
```

## 3. API ドキュメント

API ドキュメントを人手でメンテナンスしていくと，徐々に実装と乖離していき，形骸化してしまうことはよくあります．そこで，RAML や Swagger などの API ドキュメントフォーマットから JSON Schema を読み込むことで，常に最新の (現在の実装で利用されている) スキーマ定義を閲覧することができます．

以下では，RAML で JSON Schema を読み込んでいます．

```yaml:user.raml
#%RAML 0.8
title: User
version: v1.0
schemas:
  - User: !include user.json
/user:
  /{user_id}:
    uriParameters:
      user_id:
        type: number
    get:
      description: 該当する id のユーザ情報を取得
      responses:
        200:
          description: ユーザの情報が取得できた場合
          body:
            application/json:
              schema: User
        404:
          description: ユーザ情報が存在しない場合

```

RAML のドキュメント生成は [raml2html](https://www.npmjs.com/package/raml2html) がおすすめです．以下のような HTML が生成され，JSON Schema で定義したスキーマが埋め込まれて表示されています． Swagger の場合は Swagger UI で必要十分かと思います．

![raml2html](https://qiita-image-store.s3.amazonaws.com/0/7113/e324a94d-debb-95cf-251b-8087371005fa.png "raml2html")

## 4. スタブ作成

JSON Schema でスキーマ定義をおこなうと，そこからスタブオブジェクトを自動ですることができます．テストが容易になるのはもちろんのこと，例えばフロントエンド･バックエンド間で API のインタフェースのみ JSON Schema で定義しておくことで，サーバ実装とクライアント実装を並行してスムーズに進めることができます．以下では [json-schema-faker](https://www.npmjs.com/package/json-schema-faker) を利用しています．

```js
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const jsf = require('json-schema-faker')

// Schema ファイルの読み込み
const schemaFilePath = path.resolve(__dirname, 'user.yaml')
const schema = yaml.safeLoad(
  fs.readFileSync(schemaFilePath, 'utf8'),
  { schema: yaml.JSON_SCHEMA }
)

jsf.resolve(schema).then(result => console.log(JSON.stringify(result, null, 2)))
```

以下のようなスタブオブジェクトが生成されます．JSON Schema に `feker` プロパティを付与することで，対象プロパティに適した fake data を入れることができます．

```json
{
  "id": 23,
  "lastName": "Rowan",
  "firstName": "Nikolaus",
  "state": 2
}
```

# おわりに

JSON Schema を中心に据えるエコシステムを構築することによって，ドキュメントや型定義などが実装と乖離することなく，足並み揃えてアップデートできる環境を実現しました．

Protobuf や Thrift などの外部 IDL を利用してインタフェースを定義している場合でも，[jsonschema-protobuf](https://github.com/karissa/jsonschema-protobuf) のようなライブラリで JSON Schema に1度落とし込むことで，これらのエコシステムのメリットを享受できるかと思います．