Chapter 10

スキーマ(Schemas) - リクエスト

smithonisan
smithonisan
2021.08.08に更新

リクエストの定義

前章では、リクエストパラメータを取らない GET 関数を定義しました。本章では、 GET /tasks と対になる、 POST /tasks に対応する create_task() 関数を定義していきます。

POST 関数では、リクエストボディを受け取りデータを保存します。

スキーマ

先程定義したGETの関数では、 id を持つ Task インスタンスを返却していました。これに対し、通常POST関数では id を指定せず、DBで自動的に id を採番することが多いです。
また、 done フィールドに関しても、作成時は常に false であるため、 POST /tasks のエンドポイントからは除きます。

そのため、POST関数は、リクエストボディとして title のフィールドだけ受け取ることとします。POSTのために新たに iddone フィールドを持たない TaskCreate クラスを定義します。

api/schemas/task.py
class TaskCreate(BaseModel):
    title: Optional[str] = Field(None, example="クリーニングを取りに行く")

すると、 TaskTaskCreate の共通するフィールドは title のみですので、 title のみを持つ両方のベースクラスとして、 TaskBase を定義します。

api/schemas/task.py
 class TaskBase(BaseModel):
     title: Optional[str] = Field(None, example="クリーニングを取りに行く")
 
 
-class TaskCreate(BaseModel):
+class TaskCreate(TaskBase):
-    title: Optional[str] = Field(None, example="クリーニングを取りに行く")
+    pass
 
 
-class Task(BaseModel):
+class Task(TaskBase):
     id: int
-    title: Optional[str] = Field(None, example="クリーニングを取りに行く")
     done: bool = Field(False, description="完了フラグ")
 
+    class Config:
+        orm_mode = True

ここで、 orm_mode はこの後 12章 DB操作(CRUDs) でDBと接続する際に使用します。説明は 12章 DB操作(CRUDs) を参照ください。

さらに、 TaskCreate のレスポンスとして TaskCreateid だけを追加した TaskCreateResponse も定義します。

api/schemas/task.py
class TaskCreateResponse(TaskCreate):
    id: int

    class Config:
        orm_mode = True

まとめると、 api/schemas/task.py は最終的に以下のようになります。

api/schemas/task.py
class TaskBase(BaseModel):
    title: Optional[str] = Field(None, example="クリーニングを取りに行く")


class TaskCreate(TaskBase):
    pass


class TaskCreateResponse(TaskCreate):
    id: int

    class Config:
        orm_mode = True


class Task(TaskBase):
    id: int
    done: bool = Field(False, description="完了フラグ")

    class Config:
        orm_mode = True

ルーター

これを利用して、ルーターにPOSTのパスオペレーション関数 create_task() を定義していきます。

api/routers/task.py
@router.post("/tasks", response_model=task_schema.TaskCreateResponse)
async def create_task(task_body: task_schema.TaskCreate):
    return task_schema.TaskCreateResponse(id=1, **task_body.dict())

本来では、リクエストパラメータに応じてDBへの保存を行いたいところですが、ここではまずはAPIとして正しい型のデータを受け取り、正しい型でレスポンスを返すようにするため、受け取ったリクエストボディに id を付与して、レスポンスデータを返すこととします。

create_task 関数の引数に指定しているのがリクエストボディ task_body: task_schema.TaskCreate です。

先程説明したとおり、リクエストに対してレスポンスデータは id を持ちます。リクエストボディのクラス task_schema.TaskCreate をいったん dict に変換し、これらのkey/valueおよび id=1 を持つ task_schema.TaskCreateResponse インスタンスを作成します。これが task_schema.TaskCreateResponse(id=1, **task_body.dict()) です。

ここで、 dict インスタンスに対して先頭に ** をつけることで、 dictキーワード引数として展開 し、 task_schema.TaskCreateResponse クラスのコストラクタに対して dict のkey/valueを渡します。
つまり、これは task_schema.TaskCreateResponse(id=1, title=task_body.title, done=task_body.done) と等価となります。

動作確認

上記で定義したPOSTエンドポイントをコールしてみましょう。
リクエスト時のリクエストボディがそのままレスポンスに返ってきているのがわかります。

リクエストボディを以下のように変更することで、動的にレスポンスが書き換わることを確認することができます。

残りのすべてのリクエストとレスポンスの定義

ルーターには全部でパスオペレーション関数が6つしかありませんので、他の関数もすべてリクエストとレスポンスを埋めていきましょう。
最終的に、 api/routers/task.pyapi/routers/done.py は以下のようになります。

api/routers/task.py
@router.get("/tasks", response_model=List[task_schema.Task])
async def list_tasks():
    return [task_schema.Task(id=1, title="1つ目のTODOタスク")]


@router.post("/tasks", response_model=task_schema.TaskCreateResponse)
async def create_task(task_body: task_schema.TaskCreate):
    return task_schema.TaskCreateResponse(id=1, **task_body.dict())


@router.put("/tasks/{task_id}", response_model=task_schema.TaskCreateResponse)
async def update_task(task_id: int, task_body: task_schema.TaskCreate):
    return task_schema.TaskCreateResponse(id=task_id, **task_body.dict())


@router.delete("/tasks/{task_id}", response_model=None)
async def delete_task(task_id: int):
    return

done に関する関数はチェックボックスをON/OFFするだけですのでリクエストボディもレスポンスもありません。シンプルですね。

api/routers/done.py
@router.put("/tasks/{task_id}/done", response_model=None)
async def mark_task_as_done(task_id: int):
    return


@router.delete("/tasks/{task_id}/done", response_model=None)
async def unmark_task_as_done(task_id: int):
    return

スキーマ駆動開発について

さて、本章および 9章 スキーマ(Schemas) - レスポンス では、 8章 ルーター(Routers) で定義したルーターのプレースホルダーに対してリクエストとレスポンスを定義しました。しかし、まだ肝心のデータ保存やデータ読み込みが実装されていないのでAPIとしては役に立ちません。

ルーターとしてはそれぞれのパスオペレーションごとにたった3行のコードを書いたに過ぎませんが、この時点で APIモック の役割を果たします。すなわち、ここまで準備した段階で、フロントエンドとバックエンドのインテグレーションを開始することができるのです。

もちろん、条件によってリクエストやレスポンスの型が変わったり、異常ケースのすべてを扱えないことはあります。しかし、少なくとも正常ケースの1つのパターンについてすべてのエンドポイントが網羅されているのは、 2章 FastAPIの概要 でも説明したように、API開発と分離したSPAなどのフロントエンド開発者にとっては強力な武器となるでしょう。

開発初期に与える影響

多くの他のフレームワークでは、Swagger UIのインテグレーションをサポートしていません。そのため、通常は

  1. スキーマをOpen APIの形式(通常はYAML)で定義する
  2. Swagger UIを提供してフロントエンド開発者に引き渡す
  3. API開発に取り掛かる

というフローによってスキーマ駆動開発を実現しますが、FastAPIでは、

  1. API開発としてルーターとスキーマを定義し、フロントエンド開発者に引き渡す
  2. 上記のルーターとスキーマをそのまま肉付けする形でAPIの機能を実装する

というずっとシンプルなステップでスキーマ駆動開発が実現できるのです。

機能修正時に与える影響

FastAPIによるスキーマ駆動開発は思ったよりも強力です。最初に開発するときだけではなく、 最初に定義したリクエストやレスポンスを変更するフロー を考えてみましょう。

通常他のフレームワークでは、

  1. 最初にOpen APIで定義したスキーマを変更する
  2. 変更後のSwagger UIを提供してフロントエンド開発者に引き渡す
  3. APIを修正する

となるところが、FastAPIであれば

  1. 動いているAPIのリクエストやレスポンスを直接変更し、同時に自動生成されたSwagger UIをフロントエンド開発者に引き渡す

となるのです。一度APIを開発してしまうと、Open APIのスキーマ定義はメンテナンスされなくなり、例えばSwagger UIを提供するモックサーバーを立ち上げる方法が忘れ去られたり、そもそも壊れてしまったりすることがありますが、FastAPIはAPIインターフェイスの定義(ドキュメント)と実装が一緒になっているのでその心配がありません。