Python | Web フレームワーク:リクエストモデル

Python
スポンサーリンク

概要(リクエストモデルは「リクエストの設計図」)

リクエストモデルは、

「クライアントから送られてくる JSON が、どんな形・どんな型をしているべきかを表した“設計図”」

です。

FastAPI(+Pydantic)では、この「設計図」をクラスとして書いておくことで、

入力チェック(必須項目・型チェック)
自動ドキュメント(/docs に綺麗に表示)
エディタでの補完(.name など)

を一気に手に入れられます。

ここをちゃんと理解できると、
「なんとなく dict を受け取ってガチャガチャチェックするコード」から卒業して、
「型付きで安全な Web API」を作れるようになります。


なぜリクエストモデルが必要なのか(dict 直取りがつらくなる理由)

dict で直接受け取り始めると何が起こるか

リクエストボディ(JSON)を、何も考えずに dict で受け取ると、だいたいこうなります。

# 悪い意味で“ありがち”なコード
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/items")
async def create_item(request: Request):
    data = await request.json()
    name = data.get("name")
    price = data.get("price")
    if name is None or price is None:
        return {"error": "name and price are required"}
    # ここからさらに型チェックやら何やら…
    return {"name": name, "price": price}
Python

最初はこれでも動きますが、すぐに問題が出てきます。

どの項目が必須か、どの型か、コードを読まないと分からない
チェック漏れが起きやすく、バグりやすい
/ docs に自動で「このAPIはこういうJSONを受け取る」と出てこない

つまり、「仕様がコードの中に埋もれる」のがつらいポイントです。

モデルにする=「仕様を1か所にまとめて宣言する」

リクエストモデルを導入すると、この「仕様」をクラスとしてまとめて書けます。

仕様が一目で分かる(フィールド名・型・必須/任意・デフォルト)
その仕様どおりにリクエストが来なかったら、自動で 422 エラー
/ docs にそのまま表示される

「このAPIはこういう JSON を受け取ります」という契約を、
コードの中のあちこちではなく、「モデルクラス1つ」に集約できるのが本質です。


FastAPI+Pydantic でのリクエストモデルの基本

BaseModel を継承して「入力の形」をクラスで書く

FastAPI では、Pydantic の BaseModel を使ってリクエストモデル(スキーマ)を表現します。

最小例から見てみます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True

@app.post("/items")
def create_item(item: Item):
    return {
        "message": "created",
        "item": item
    }
Python

このコードの意味を、丁寧に分解します。

Item クラスが「リクエストボディの設計図」です。
name: str → name は必須の文字列
price: float → price も必須の数値(float)
in_stock: bool = True → in_stock は任意の真偽値(省略時は True)

エンドポイント側で item: Item と書いているのがポイントです。
FastAPI は、「このエンドポイントはリクエストボディを Item 型として受け取るんだな」と理解します。

クライアントは例えば次の JSON を送ります。

{
  "name": "Apple",
  "price": 1.5
}

in_stock を省略しても、デフォルト値 True が入ります。

FastAPI が裏側でやってくれていること

この item: Item と書くだけで、FastAPI+Pydantic が裏でやってくれることはかなり多いです。

JSON を Python の dict にパースする
dict を Item モデルに変換する
必須項目が全部あるかチェックする
型が正しいかチェックする(文字列に float を入れてないか、など)
エラーがあれば 422 (Unprocessable Entity) のレスポンスを返す

例えば、

{
  "name": "Apple",
  "price": "abc"
}

のような JSON を送ると、price が float に変換できないのでエラーになります。

自分で if 文を書かなくても、モデル定義通りに自動でバリデーションしてくれる。
ここがリクエストモデルの一番おいしいところです。


必須/任意/デフォルトの表現(ここをしっかり理解する)

必須フィールド(必ず入れてほしい項目)

Item モデルで、

class Item(BaseModel):
    name: str
    price: float
Python

と書いた場合、name と price は必須です。

クライアントが次のように送ってきたら、

{
  "name": "Apple"
}

price が欠けているので、FastAPI は 422 エラーを返します。

つまり、「型ヒントが書かれているフィールド」は、
デフォルト値を書かない限り「必須」とみなされます。

任意フィールド(あってもなくても良い項目)

任意にしたいときは、主に 2 通りの書き方があります。

ひとつは、デフォルト値を付ける方法。

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True
Python

この場合、

in_stock が省略されたら True
in_stock が指定されればその値(true / false)

になります。

もうひとつは、「本当に指定されないことがある(None も許す)」という意味の Optional です。

from typing import Optional

class Item(BaseModel):
    name: str
    price: float
    description: Optional[str] = None
Python

これだと、

description を送ってもいいし、送らなくてもいい
送られなければ None

という扱いになります。

必須なのか、あってもなくてもいいのか、
なくてもいいなら「デフォルト値を持たせるか」「None を許すか」
このあたりを意識して設計すると、モデルがかなり洗練されます。


実用寄りの例:ユーザー登録 API のリクエストモデル

例:ユーザー登録に必要な情報をモデル化する

ユーザー登録 API を考えてみます。
欲しい情報は例えばこんな感じでしょうか。

ユーザー名(必須)
メールアドレス(必須)
年齢(任意)
お知らせメールを受け取るかどうか(任意、デフォルトは受け取る)

これを Pydantic モデルにすると、こう書けます。

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import Optional

app = FastAPI()

class UserCreate(BaseModel):
    username: str
    email: EmailStr
    age: Optional[int] = None
    allow_notifications: bool = True

@app.post("/users")
def create_user(user: UserCreate):
    return {
        "message": "user created",
        "user": user
    }
Python

ここで大事なところを深掘りします。

ひとつ目は、email に EmailStr を使っていることです。
Pydantic が提供する「メールアドレス用の型」です。
「@ が入っていない」「明らかにメールアドレスでない」文字列を弾いてくれます。

ふたつ目は、age を Optional[int] にしていること。
年齢は任意なので、指定されなければ None。
指定されれば int として扱われます。

みっつ目は、allow_notifications にデフォルト True を持たせていること。
送られなければ受け取る前提(True)、明示的に False を送ることもできます。

このモデルを見れば、「この API は何を受け取りたいのか」が一目で分かります。
コードを追いかけなくても、UserCreate だけを見れば仕様を把握できる。
これが「リクエストモデルに仕様を集約する」メリットです。


ネストしたリクエストモデル(住所などの複雑な構造)

モデルの中にモデルを入れる

現実の API では、単純な平べったい JSON だけでなく、
「中にオブジェクトを持つ JSON」もよく出てきます。

例えば、ユーザーの住所を別のモデルにして入れたいケース。

{
  "username": "taro",
  "email": "taro@example.com",
  "address": {
    "postal_code": "123-4567",
    "prefecture": "Tokyo",
    "city": "Koto-ku"
  }
}

これを Pydantic モデルで表現してみます。

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

class Address(BaseModel):
    postal_code: str
    prefecture: str
    city: str

class UserCreate(BaseModel):
    username: str
    email: EmailStr
    address: Address

@app.post("/users")
def create_user(user: UserCreate):
    return {
        "message": "user created",
        "user": user
    }
Python

ここでは、

Address モデルが「住所の仕様」
UserCreate の address: Address が、「ユーザーの中に住所を持つ」

ことを表しています。

FastAPI+Pydantic は、
外側の UserCreate だけでなく、中の Address についてもバリデーションしてくれます。

postal_code を省略したり、city に数値を渡したりするとエラーになります。

複雑な JSON ほど、モデルを分割してネストしてあげると読みやすくなります。


実務で効くポイント:リクエストモデルを「ドキュメント」として扱う

/docs にそのまま「仕様」が表示される

FastAPI アプリを起動して /docs を開くと、
エンドポイントごとに「Request body」としてモデルの構造が表示されます。

どんなフィールドがあるのか
必須か任意か
型は何か(string, number, boolean, object, array など)

が視覚的に確認できます。

これは、そのまま「API仕様書」として使えます。

モデルを少し直して再起動すれば、 /docs も自動で更新されます。
つまり、

仕様変更 = モデル定義の変更 = ドキュメントの自動更新

という流れになります。

仕様書だけ古い問題(あるある)を防げるのは、大きなメリットです。

IDE の補完と型チェックも効く

コードの中で、リクエストモデルを使うと、
エディタで型補完が効くようになります。

例えば、

@app.post("/items")
def create_item(item: Item):
    total = item.price * 1.1
    # item.name, item.in_stock などが補完される
Python

dict だと "name""price" を手で打ちますが、
モデルだと item. と打てば候補が出ます。

タイプミスで "prcie" と書いても気づきづらい dict 形式より、
モデル形式のほうがミスも減ります。

また、mypy などの型チェッカーを使うときも、
リクエストモデルが「ちゃんとした型」として扱われるので、
静的解析もしやすくなります。


まとめ(リクエストモデルは「安全で分かりやすい入力の入口」)

リクエストモデル(FastAPI+Pydantic)の本質を整理すると、こうなります。

リクエストモデルは、クライアントから送られる JSON の「形・型・必須・任意」をクラスとして表現した“仕様の設計図”。
BaseModel を継承したクラスにフィールド名と型ヒントを書くことで、入力チェック(バリデーション)を自動化できる。
必須項目はデフォルトなし、任意項目はデフォルト値や Optional で表現し、「何が必須で何がオプションか」がモデルだけで分かる。
ネストしたモデル(Address を User の中に持つなど)を使うことで、複雑な JSON も読みやすく・安全に扱える。
リクエストモデルは /docs にそのまま反映され、IDE 補完や型チェックも効くので、「コード=ドキュメント=バリデーション」という理想的な状態に近づける。

タイトルとURLをコピーしました