committee + prmd でJSON Schemaをいい感じに運用する

FiNC Developer Advent Calendar 2016の19日目の記事です。

要約

  • リクエスト・レスポンス形式のドキュメントはメンテが大変
  • メンテされていなくても気がつかないため放置され気味
  • committeeでJSON Schemaに実装が沿っているかを確認可能
  • prmdでJSON Schemaを楽に書ける+人が読めるドキュメントが生成できる
  • メンテされていない事が検出できるので続く(はず

API作成時のリクエスト・レスポンス形式問題

サーバとクライアントで並行開発しながらAPIを作成する場合、以下のようにリクエスト・レスポンス形式を整えにくいという問題があります。

  • リクエストの形式が想定と違う
    • 想定外のキーを送ってくる
    • 構造が想定していない構造
  • レスポンスに知らないキーが含まれている
  • 同じデータなのにエンドポイントによって形式が違う
    • enumの数値だったり、対応する文字列だったり
    • カテゴリ名を返していたりカテゴリIDを返していたり

コミュニケーションや、APIに関するドキュメントをきちんとメンテしていれば回避可能な問題ですが、
以下の2つを保証する必要があり、何のサポートなしに維持するのはとても大変です。

  • 形式が全てドキュメント化されている
  • ドキュメントと実装が揃っている事を保証する
  • 同じデータは同じ形式になる事を保証する

そこで、今回話題にするJSON Schema+committee+prmdを導入することで、この問題を大幅に解決できます。

JSON Schemaで仕様を定義する

JSON SchemaとはJSONを用いてデータを記述する際の形式の1つで、主にアノテーションやバリデーションを目的としているものらしいです。
詳しくはこちら
http://json-schema.org/

例えば以下のように定義すると、GET /v1/friendsが、access_tokenをパラメータとして必ず取り、戻り値には必ずfriendsとreccommendsというキーが含まれているという事を定義できます。 また、friendsとreccomendsの具体的な内容については別に定義して読み込みますが、同じ景色であるということも表現しています。 (なお、実際はもっと情報が多く、この部分だけでは動きません)

{
  "title": "show self friends with recommends",
  "href": "/v1/friends",
  "method": "GET",
  "schema": {
    "type": "object",
    "required": [
      "access_token"
    ],
    "properties": {
      "access_token": {
        "type": "string"
      }
    }
  },
  "targetSchema": {
    "type": "object",
    "required": [
      "friends",
      "recommends"
    ],
    "properties": {
      "friends": {
        "$ref": "#/definitions/user/definitions/friends"
      },
      "recommends": {
        "$ref": "#/definitions/user/definitions/friends"
      }
    }          
  }
}

JSON Schemaにそって定義を書くことで、形式が統一されプログラムから処理しやすくなるのと、他の場所の定義を共有できるため、
同じデータなのに人によって違う構造になるような問題をを解決しやすくなります。

committee

JSON Schema自体はただの形式なのでそれ自体は特に何も無いですが、統一された形式になるため、容易にプログラムから処理を行う事ができます。

rubygemのcommitteeはその発想に沿ったものです。
このgemはRackのミドルウェアとして動作し、JSON Schemaを読み込ませることで、
リクエストやレスポンスが定義にそっているかをチェック、そっていない場合にエラーにするなどの処理を行います。
また、テストにおいても形式が正しいかをチェックできます。

このgemにより、必須パラメーターが足りない場合や、違うデータが入っている場合にエラーを起こすことができ、
JSON Schemaの形式と実際の形式とがそろっていることを保証できます。

さらに、JSON Schemaには形情報も書けることを利用し、数値でくるべきところが文字列だった場合にエラーを返すことや、
日付フォーマットの文字列をdatetimeに変換、GETリクエストの時に文字列を数値に変換してくれたりと色々な便利機能があります。

JSON Schema+committeeの利点と問題点

JSON Schemaとcommitteeを利用することで、以下が可能になります。

  • レスポンス・リクエスト形式を構造化できる
  • 実装がJSON Schemaに沿っているか自動チェック&変換できる

これで、当初の問題だったドキュメントが実装と乖離していく問題は大幅に解決が可能ですが、代わりに以下の問題が出てきます。

  • JSON Schemaを人間が書くのが大変
  • JSON Schemaを人間が読むのもつらい

JSONは人間が直接読み書きするドキュメントには(たぶん)適さないです。
そのため、たとえ利点があったとしてもJSON Schemaを作成するのがとても大変になってしまいます。

それを解決するのがprmdです。

prmd

prmdはJSON SchemaをYAMLで定義することや、複数のファイルに分割して定義、
JSON Schemaを元に人間が読めるmarkdownドキュメントを生成できるgemです。

prmdでは以下のようにYAMLを書くことで、先ほどのようなJSON Schemaを出力できます。

- title: show self friends with recommends
  href: /v1/friends
  method: GET
  rel: show
  schema:
    type: object
    required:
      - access_token
    properties:
      access_token:
        type: string
  targetSchema:
    type: object
    required:
      - friends
      - recommends
    properties:
      friends:
        $ref: #/definitions/user/definitions/friends
      recommends:
        $ref: #/definitions/user/definitions/friends

また、マークダウンのドキュメントも以下のように出してくれます。

[](/image s/blog/2016/2016-12-19-prmd_document.png)

これにより、JSON Schemaを人間が読み書きできるようになります。

実際の開発手順

以下のように進めると、JSON Schemaの利点を最大限活用できると思います。

  1. 実装前にYAMLでレスポンス・リクエスト形式を定義
  2. prmdでJSON Schemaとmarkdownを出力
  3. markdownはクライアント側に事前共有
  4. JSON Schemaの通りに動いているかテストしつつ開発

という流れで、リクエスト・レスポンス形式に関係する部分は、かなりしっかりと開発を進めることができ、
平行して開発を行っても手戻りやコミュニケーションロスを大幅に減らせます。

全体としては以下の図のように動いています。

[](/image s/blog/2016/2016-12-19-committee_prmd.png)

まとめ

解決すること

正しく導入することで以下の問題を解決できます。

  • 実装とドキュメントが乖離しないこと
  • データ構造を揃えること

解決しないこと

今回のはあくまでリクエスト・レスポンス形式に絞られているため、 以下の問題は解決しません。

  • レスポンスのデータの内容が意図通りであること
    • 形式はあっているが振る舞いが想定しているものかはチェックしません
  • エラー形式の保証
    • レスポンス形式は一種類のみで、異常系は対象外です…
  • リクエスト・レスポンス以外のドキュメント
    • 仕様やデータの意味等は対象外です

未解決問題

以下のような問題が出ており、解決方法を模索中です。

  • ドキュメントの読みやすさ
  • クライアント側へ配布するアクセス用ライブラリの生成
  • バージョニング対応

最後に

JSON Schema+committee+prmdは全てを解決するものではありませんが、クライアント・サーバ間の形式という、認識の齟齬が起こりやすい境界の部分にはとても効果がある組み合わせでした。