marshmallow, flasggerを使ってdocstringからSwaggerUIを自動生成する。

# はじめに
最近はFlaskを使用したWeb APIを実装することが多いです。
Web APIを提供する側としては、なるべく手間をかけずにAPIドキュメントも整備したいですよね。
この記事では、marshmallowを利用した入力値の検証／出力値の整形を行いつつ、docstringに記載したSwagger YAMLからSwaggerUIを自動生成する手法を紹介します。

# 開発環境
[サンプルのリポジトリ](https://github.com/taguchi8s/marshmallow-with-flasgger)を用意しています。
クローンして以下のコマンドで実行可能です。

```
$ pipenv install
$ pipenv run dev
 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 325-268-230
```

# 解説

## [marshmallow](https://github.com/marshmallow-code/marshmallow)
marshmallowはオブジェクトのシリアル化をサポートするライブラリです。
今回はmarshmallowを利用して入力値の検証、出力値の整形を行います。

まずは基本となるリクエスト／レスポンスを表すスキーマを定義しましょう。
ここでは、あるユーザーを表すスキーマ`UserSchema`を定義しています。属性には制約も追加しておきます。

```python:schema.py
from typing import List, Tuple, Type

from marshmallow import Schema, fields, validate


class UserSchema(Schema):
    user_name = fields.Str(required=True, validate=validate.Length(max=10))
    mail = fields.Str(required=True)
    age = fields.Int(required=False, validate=validate.Range(min=0))


def get_all_schemas() -> List[Tuple[str, Type[Schema]]]:
    """
    定義したスキーマの一覧を返す関数です。
    flasggerに登録してSwagger YAMLで利用可能にするために利用します。
    :return:
    """
    return [(UserSchema.__name__, UserSchema)]
```

|属性名|説明|制約|
|:-:|:-:|:-:|
|user_name|ユーザーの名前|必須／最大10文字|
|mail|メールアドレス|必須|
|age|年齢|任意／0歳以上|

## [flasgger](https://github.com/flasgger/flasgger)
flasggerはflaskで定義されたビューからOpenAPIの仕様を作成するライブラリです。
また[SwaggerUI](https://swagger.io/tools/swagger-ui/)を追加してくれる機能も備わっています。
先ほど定義したmarshmallowのスキーマを登録して、Swagger YAMLでスキーマを利用できるようにしておきます。

```python:main.py
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from flasgger import APISpec, Swagger
from flask import Flask

from schema import get_all_schemas

spec = APISpec(
    title="marshmallow-with-flasgger",
    version="0.1.0",
    openapi_version="2.0",
    plugins=[FlaskPlugin(), MarshmallowPlugin()],
)

flask_app = Flask(__name__)
flask_app.config["JSON_AS_ASCII"] = False
flask_app.config["SWAGGER"] = {"uiversion": 3}
flask_app.config["SECRET_KEY"] = "secret key"

_template = spec.to_flasgger(flask_app, definitions=get_all_schemas())  # ここで先ほどのget_all_schemas関数を呼び出しています。
_swag = Swagger(flask_app, template=_template)
```

エンドポイントのdocstringにSwagger YAML形式でドキュメントを記載します。
ユーザー登録のエンドポイントでリクエストには`UserSchema`を受け取ります。
`#/definitions/UserSchema`と記載することでリクエスト／レスポンスでのスキーマの利用が可能になっています。

```python:main.py
@flask_app.route("/users", methods=["POST"])
def post_users():
    """
    post user
    ---
    tags:
      - user
    produces:
      - application/json
    parameters:
      - in: body
        name: user
        schema:
          $ref: '#/definitions/UserSchema'
    responses:
      200:
        description: Successful operation
        schema:
          $ref: "#/definitions/UserSchema"
    """
    payload = request.json

    # 入力値の検証を行います。入力値がSchemaと一致しない場合はValidationErrorがraiseされます。
    schema = UserSchema()
    schema.load(payload)

    # バリデーションが完了したら何らかのロジックを実行するでしょう。
    db.Users.save(schema.dump(payload))

    return jsonify(payload)
```

入力値がSchemaと一致しない場合はValidationErrorがraiseされます。
ValidationErrorはエラーハンドラーでまとめてキャッチしておくと便利です。

```python:main.py
@flask_app.errorhandler(ValidationError)
def handle_schema_validation_error(e: ValidationError):
    return jsonify(e.messages), 400
```

# 動作確認

http://localhost:8080/apidocs/ にアクセスします。
flaskのビュー、marshmallowで定義されたスキーマから自動生成されたSwaggerUIのページを確認できます。
入力値の検証のために定義した`UserSchema`の制約が、ドキュメントにも自動的に反映されていることがわかります。

![localhost_8080_apidocs_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134694/0a3be90a-1fab-addd-ff47-06a39fcd9d38.png)

## リクエストを投げてみる

Try it out でリクエストを送信してみます。
実行画面ではdocstringで定義した`#/definitions/UserSchema`からリクエストの雛形が作成されるので便利です。Executeで送信が可能です。

![localhost_8080_apidocs_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134694/44bbb910-9288-8dae-1935-11cf08e6a3b6.png)

また`UserSchema`で定義された制約に違反するリクエストを送信すると、marshmallowがraiseしたエラーが表示されます。
以下は年齢を-1として送信した結果です。

![スクリーンショット 2020-12-05 18.29.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134694/1d39bdc9-91ce-f5d2-ea3c-67425a3e0249.png)

# まとめ

環境を一度ここまで整備すれば、
1. 入力値の検証のためにスキーマを書く
2. APIを実装しdocstringを書く
二つの自然な手順でドキュメントの自動生成までスムーズに行うことができます。スキーマの制約を更新したり、新しい属性を追加した際のドキュメントの更新漏れもなくなるでしょう。
快適なドキュメントの自動生成の仕組みが備わっていると、APIを提供する側、利用する側共に気持ちよく開発できますね。
DX（Digital Transformation）だけでなく、DX（Developer Experience）も大切にしていきたいと思います。（これが言いたかっただけ）