JSON Schema による矛盾しないドキュメント作成の勧め

今の職場で働き始めてちょうど一年が経ちました。そこでとても幸運なことに、ある一つのサービスの開発に設計段階からかかわることができました。

そのサービスは大まかに分けるとWebアプリケーションのフロント部分(Vue.jsなど)・APIサーバー・データ処理サーバー・AIサーバーと分かれており、自分は主にAPIサーバーを担当していました。

APIサーバーはメタデータなどの保存の役割も持っており、フロント・データ処理サーバー・AIサーバーにそれぞれAPIの提供をしています。APIはREST(風)で提供していますが、その数は膨大で、しかも複数人でそれぞれの開発を行うためAPIの仕様に関してコミュニケーションコストをできるだけ低くする必要がありました。そこで始めに導入したのがアプリケーションと差異の出ない動的ドキュメントです。

サンプルは [github.com/itkr/documentation-sample](https://github.com/itkr/documentation-sample) にあります。

## 動的ドキュメントの概要

1. 各APIのリクエストとレスポンスの値を JSON Schema でバリデート
1. 各エンドポイントをリストにして表示
1. スキーマとそこから自動生成したサンプルを表示
1. さらに説明が必要な箇所はコード内にreStructuredTextで記述

## JSON Schema によるリクエスト・レスポンスの厳格化

まず [JSON Schema](https://json-schema.org) を使ってリクエストとレスポンスのバリデーションを厳密に行うようにしました。リクエストでバリデーションをかけるのはフロントからの不正な値を受け付けないためですが、レスポンスでバリデーションをかける理由はドキュメントと実際に返す値がずれてしまうことに開発段階で気づくためです。

JSON Schema を使った理由は、ある程度複雑なデータに対応できること・JSONファイルで定義するので別ファイルにできること・多くの言語で利用できること・広く使われていることなどです。

## エンドポイントの自動ドキュメント化

新しいAPIを作った時には、そのエンドポイント＋メソッドに対してドキュメントの書き忘れがないように自動でドキュメントが作成されるようにしています。これはSphinxで静的なドキュメントを作るようなことではなく、Webアプリケーションとして動的に作られます。

作成方法は、エンドポイントを定義するときにそれをリスト化しておき、それぞれのエンドポイントの実装(クラス)に必ずスキーマとサンプルを取得するメソッドを用意して、それがなければエラーになるようにしています。


```python
class SpamHandler(Handler):

    @classmethod
    def get_request_schema(cls, method=None):
        return {
            # スキーマ
        }

    # ...
```

![スクリーンショット 2018-11-24 22.46.10.png](https://qiita-image-store.s3.amazonaws.com/0/14918/e7c6fe14-6f3d-9dbd-98af-b9aa1478ab85.png)

## サンプルの実装

スキーマのJSONにはサンプル用のキーを作って、それをもとにサンプルを作成するように独自に実装しています。できればここは独自実装ではなくいい感じのライブラリを利用したいところです。できるだけここで作るサンプルはそのままアプリケーションにリクエストとして渡すだけで動くことを目指しています。

schema.json

```schema.json
{
    "type": "object",
    "properties": {
        "foo": {
            "type": "string",
            "examples": ["sample string"]
        },
        "bar": {
            "type": "string"
        }
    }
}
```

↓

sample.json
memo: Actually, There is a sample created automatically or manually.

```sample.json
{
    "foo": "sample string",
    "bar": ""
}
```

![スクリーンショット 2018-11-24 22.54.18.png](https://qiita-image-store.s3.amazonaws.com/0/14918/b06acd9b-0909-2a49-6421-a6d7896c221b.png)

## テストでのスキーマ・サンプルの利用

テストは様々な入力を想定するべきなのでこのサンプルのJSONを使うだけでは不十分だとは思いますが、自動生成されたリクエストサンプルを入力値として、実際に返ってきたレスポンスとレスポンススキーマを比較するというテストを各APIにひとつ書いておけば、少なくともドキュメント通りに動くかどうかはテストできます。

## ソースコード内のカスタムドキュメント

スキーマとサンプルの表示だけではAPIの詳細な使い方を説明するには不十分だという場合もあるので、カスタムドキュメントをコード内に記述できるようにしています。Pythonで書いているのでクラスの定義にreStructuredTextで記述し、 `docutils` でHTMLに変換したものをスキーマ・サンプルと合わせて表示しています。

```python
class SpamHandler(Handler):
    """
    SpamHandler
    -----------

    サンプルのハンドラーです
    """

    # ...
```

![スクリーンショット 2018-11-24 22.46.51.png](https://qiita-image-store.s3.amazonaws.com/0/14918/4889be0a-a06a-54c2-35c7-7ed759b7c945.png)

## ドキュメント化するときの JSON Schema の記述のコツ

### `additionalProperties` に `false` を入れる

`additionalProperties` は、スキーマに記述していないキーを受け付けるかどうかを指定するものです。 `false` を入れることで想定していないキーが入力されたときにバリデーションエラーを発生させます。こうすることでよりスキーマを厳格化できます。

### `requirements` を指定する

`requirements` は必ず無ければならないキーを指定ます。これを指定することでキーが不足しているJSONをバリデーションエラーにすることができます。

## JSON Schema の導入で得られたこと

### JSONをデータとして扱いやすくなった

扱っているデータの中には機械学習の手順を定義するような複雑なデータもあります。それらはJSONで定義して保存していますが、JSON Schema でバリデーションをかけることでプログラムを書くことなく複雑なJSONにも対応しやすくなりました。

### API設計者とデータ設計者が別の人でも開発できた

例えば学習手順の定義を記述したJSONをクライアントで作成し、それを保存するだけのAPIがあった場合に、APIの設計者は必ずしも学習手順に精通していなくても精通している人が書いたスキーマを割り当てるだけでAPI自体の開発が可能なため、API設計者とデータ設計者が別の人でも開発ができます。

### バリデーションが言語やフレームワークに依存しなくなった

現在Pythonで書かれているコードをGoで書き直すということをやっていますが、PythonコードでのバリデーションではなくJSONファイルによるバリデーションなので使い回しが可能です。