Chapter 09

スキーマ(Schemas) - レスポンス

smithonisan
smithonisan
2021.09.02に更新

スキーマ(Schemas)には、APIのリクエストとレスポンスを、厳密な型と一緒に定義していきます。本章では、まずレスポンスについて学んでいきます。

型ヒント

ご存知の通り、Pythonは動的型付け言語です。しかし、昨今の動的型付けであっても型を重視するトレンドの例に漏れず、Pythonでは 「型ヒント(Type Hint)」 を使って関数のシグネチャなどに型を付与することが出来ます。

通常、型ヒントは実行時に影響を及ぼさず(コードの中身には何も作用せず)、IDEなどに型の情報を与えるものです。しかし、FastAPIでは、依存するPydanticという強力なライブラリによって、この型ヒントを積極的に利用し、 APIの入出力のバリデーション を行います。

「実行時に影響を及ぼさない」とはどういうことでしょうか?
例えば int 型であることを期待して変数 num を以下のように定義したとします。

>>> num: int = 1

しかし、ここに誤って文字列型の "string" を代入しようとします。

>>> num = "string"

Pythonは何もエラーを発しません。あくまでも最初に与えた : int は型ヒントであって、静的型付け言語のように変数の型を限定するものではありません。その証拠に、以下のように変数の型は str となっているのがわかります。

>>> num
'string'
>>> type(num)
<class 'str'>

レスポンス型の定義

それでは、先程のパスオペレーション関数にレスポンス型を定義していきましょう。リクエスト型については後ほど扱います。(10章 スキーマ(Schemas) - リクエスト)。

api/schemas/task.py を以下のように作成します。

api/schemas/task.py
from typing import Optional

from pydantic import BaseModel, Field


class Task(BaseModel):
    id: int
    title: Optional[str] = Field(None, example="クリーニングを取りに行く")
    done: bool = Field(False, description="完了フラグ")

このファイルは、FastAPIのスキーマを表します。APIのスキーマは、APIのリクエストやレスポンスの型を定義するためのもので、 11章 に登場するデータベースのスキーマとは異なることに注意しましょう。

それぞれのクラス定義については後ほど説明します。先に、このスキーマを利用して実際にAPIレスポンスが返却できるか確認してみましょう。前章で作成した api/routers/task.pylist_tasks() 関数を以下のように書き換えます。

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

ここでは api.schemas.tasktask_schema と読み替えてimportしています。この後DBと接続する際のモデル(models)を定義したときに、同名のファイル api/models/task.py を定義し、こちらを task_model と読み替えて区別するためです。

これで、Swagger UIからアクセスすると、APIにレスポンス(Response body)が追加されたことが確認できます 🎉

レスポンス型定義の説明

それでは先程定義したスキーマの中身を説明していきます。

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

BaseModel はFastAPIのスキーマモデルであることを表すので、このクラスを継承して Task クラスを作成しています。

Task クラスは id, title, done の3フィールドを持ちます。この際、それぞれのフィールドにはそれぞれ int, Optional[str], bool の型ヒントが付加されています。

また、右辺の Field はフィールドに関する付加情報を記述します。最初の変数はフィールドのデフォルト値を表します。 titleNonedoneFalse をデフォルト値に取っているのがわかります。

example はフィールドの値の例をとります。 title は各TODOタスクのタイトル、すなわち

  • クリーニングを取りに行く

の文字列部分になります。

done は完了フラグを表します。それを説明するのが引数の description です。

これらのスキーマ定義は、Swagger UIの下部からも確認できます。

ルーターに定義したレスポンスの説明

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タスク")]

ルーターでは、先程定義したスキーマを利用して、APIのリクエストとレスポンスを定義していきます。
GET /tasks ではリクエストパラメータやリクエストボディは取りませんので、レスポンスだけを定義します。

レスポンスのスキーマとして、パスオペレーション関数のデコレータに response_model をセットします。
GET /tasks は、スキーマに定義した Task クラスを複数返しますので、リストとして定義します。ここでは、 response_model=List[task_schema.Task] となります。

現時点ではまだDBなどとの接続はなく、Taskデータの保存は考慮されていない状態です。ひとまずダミーのデータを常に返却する関数として定義しておきます。

idtitle を任意の内容にし、 done はデフォルトで False なのでここでは指定しません。
ダミーデータとして、 [task_schema.Task(id=1, title="1つ目のTODOタスク")] を返却しておきます。

型定義の強力さ

本章ではスキーマを定義してきました。スキーマを表すクラスの各フィールドには、厳密に型ヒントを付与してきました。

本章冒頭で、FastAPIにおいて型ヒントは単なるIDEの型チェックだけのためではなく、実行時の評価にも使われる、と説明しました。

その証拠に、以下のように title の型定義を Optional[str] から Optional[bool] に変更して、Swagger UIからAPIをコールしてみてください。

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

レスポンスが Internal Server Error に変わったはずです。

コンソールを確認すると、

pydantic.error_wrappers.ValidationError: 1 validation error for Task
title
  value could not be parsed to a boolean (type=type_error.bool)

とレスポンスのvalidationにfailしているのがわかります。

レスポンスだけではAPIの型定義のありがたみがわかりづらいかもしれません。この後説明していくリクエストの型ではその力をもっと発揮します。