Python | Web フレームワーク:レスポンスモデル

Python
スポンサーリンク

概要(レスポンスモデルは「返していい形の“ふるい”」)

レスポンスモデルは、

「この API は、こういう形・こういう型の JSON を“返す”はずだ」

という“約束(スキーマ)”をコードで表現したものです。

FastAPI では、Pydantic モデルを response_model=... で指定することで、

  • 返す JSON の“型”を決める
  • 余計なフィールドを自動で落とす(情報漏えい防止)
  • /docs に「この API はこういう JSON を返します」と表示してくれる

といったメリットが得られます。

リクエストモデルが「受け取る側の設計図」だとしたら、
レスポンスモデルは「外に出してよい形の設計図」です。


FastAPI におけるレスポンスモデルの基本

response_model 引数で「返り値の型」を宣言する

FastAPI では、エンドポイントを定義するときに response_model を指定します。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    id: int
    name: str

@app.get("/items/{item_id}", response_model=Item)
def get_item(item_id: int):
    return Item(id=item_id, name=f"item-{item_id}")
Python

ここでのポイントは、

  • Item モデルが「レスポンスの形」の定義
  • response_model=Item が「このエンドポイントは Item を返すべき」という宣言

になっていることです。

実際の JSON レスポンスは、例えば /items/1 に対して

{
  "id": 1,
  "name": "item-1"
}

のようになります。

関数の戻り値として Item インスタンスを返してもいいし、
{"id": ..., "name": ...} のような dict を返しても、
最終的に FastAPI が Item の形に合わせて整形してくれます。

リストやネスト構造も response_model で表現できる

複数件を返したいときは、リスト型を使います。

from typing import List

class Item(BaseModel):
    id: int
    name: str

@app.get("/items", response_model=List[Item])
def list_items():
    return [
        {"id": 1, "name": "Apple"},
        {"id": 2, "name": "Banana"}
    ]
Python

response_model=List[Item] と書くことで、

  • レスポンスは「Item 型の要素を持つリスト」
  • /docs にも「配列で返るよ」という形で表示

されます。


なぜレスポンスモデルが必要なのか(辞書をそのまま返すのと何が違う?)

「内部の情報全部」を返してしまう事故を防ぐ

ありがちなパターンとして、

def get_user_from_db(user_id: int) -> dict:
    return {
        "id": user_id,
        "name": "Taro",
        "email": "taro@example.com",
        "password_hash": "xxxxx",
    }

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return get_user_from_db(user_id)
Python

のように書いてしまうと、
password_hash のような「外に出したくない情報」まで
そのまま JSON としてクライアントに返ってしまうことがあります。

レスポンスモデルを使うと、「外に出してよいフィールド」だけを選別できます。

from pydantic import BaseModel

class UserPublic(BaseModel):
    id: int
    name: str
    email: str

@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
    user = get_user_from_db(user_id)
    return user  # password_hash があっても、レスポンスからは落ちる
Python

get_user_from_db が返した dict には password_hash が含まれていても、
FastAPI は UserPublic に存在しないフィールドを自動で除外します。

これがレスポンスモデルの強力なポイントです。

  • 内部では「いろいろ持っている」
  • 外には「モデルで定義されたものだけ」

という“ふるい”を response_model が担ってくれます。

「返り値の形」をきちんと固定することで、フロント側が楽になる

レスポンスモデルを定義しておくと、
API を使う側(フロントエンドや別サービス)が

  • どんなキーが必ずあるか
  • どの型(数値か文字列か)なのか

を事前に知ることができます。

FastAPI の /docs を開くと、

  • Request body
  • Responses → 200 → Schema(=レスポンスモデル)

が表示されるので、
「この API は成功するとこういう JSON を返すんだな」と一目で分かります。

レスポンスモデルをサボると、

  • 場合によってフィールドが増えたり減ったり
  • 型がブレる(数値と文字列が混じる)

といった“ブレた API”になりやすく、
利用側が壊れやすくなります。


内部モデルとレスポンスモデルを分ける(本当に大事な設計ポイント)

内部では「何でもあり」、外では「制限された形」にする

現実のアプリでは、
DB モデルや内部用モデル(ドメインモデル)には、
外に出したくない情報も含まれます。

例えば、ユーザーのモデルを考えてみます。

from pydantic import BaseModel

class UserInternal(BaseModel):
    id: int
    name: str
    email: str
    password_hash: str
    is_admin: bool
Python

これをそのまま response_model にしてしまうと、
password_hashis_admin まで外に出てしまいます。

そこで、外向けの「公開用モデル」を別に用意します。

class UserPublic(BaseModel):
    id: int
    name: str
    email: str
Python

エンドポイントでは、内部モデルを返しつつ、
response_model=UserPublic を指定します。

@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
    user = get_user_internal(user_id)  # UserInternal を返す想定
    return user
Python

FastAPI は、

  • 戻り値を一度 UserPublic として解釈
  • UserPublic にないフィールド(password_hash, is_admin)は削除

した上で JSON を返します。

こうすることで、

  • 内部ロジックや DB の都合でモデルが少し変わっても
  • 外向けのレスポンスの型は変えない

という設計ができます。

この「内部モデルと公開モデルの分離」は、
アプリが大きくなればなるほど効いてきます。

レスポンスモデルを「API バージョンごとの契約」として扱う

API を長く運用していると、「レスポンスの形式を変えたい」場面が出てきます。

そのときに、

  • 内部構造は自由に変えつつ
  • 古いクライアント向けには、昔のレスポンスモデルを維持
  • 新しいクライアント向けには、新しいレスポンスモデルを使う

という戦略が取れます。

バージョンごとにレスポンスモデル(UserV1, UserV2 など)を用意しておけば、
/v1/users/.../v2/users/... で違うモデルを response_model にするだけで済みます。


レスポンスモデルとバリデーション(「出す側」にも型の責任を持たせる)

response_model に合わない構造・型を返そうとしたら?

レスポンスモデルは、「出ていくデータ」にも型制約をかけます。

例えば、

class Item(BaseModel):
    id: int
    price: float

@app.get("/items/{item_id}", response_model=Item)
def get_item(item_id: int):
    return {"id": "abc", "price": "100"}
Python

このように、id を文字列 "abc" にしてしまうと、
Pydantic が「int に変換できない」と判断してエラーになります。

多くの場合、FastAPI は必要に応じて型変換を試みますが、
どうしても無理な場合はエラーになります。

これによって、

  • 内部コードのバグ(型の間違い)
  • 間違ったフィールド名や構造

が早期に発覚します。

リクエスト側だけでなく、
レスポンス側にも「型の約束」を課すことで、
API 全体の信頼性が上がります。

一部だけ違う形で返したいとき(例:エラー時の専用モデル)

成功時だけでなく、

  • 特定の成功パターン
  • 部分成功
  • エラー時

などに専用のレスポンスモデルを使いたいときもあります。

例えば、「バリデーションに通った件数」と「失敗した件数」を返すバッチ API。

from pydantic import BaseModel

class BatchResult(BaseModel):
    success_count: int
    failure_count: int

@app.post("/batch", response_model=BatchResult)
def run_batch():
    # 何らかの処理...
    return {"success_count": 10, "failure_count": 2}
Python

こうすると、
クライアント側は「この API は常に success_count と failure_count を返す」と期待できます。

エラー時は HTTP ステータスコードとエラーモデルを組み合わせて
設計することもできますが、
まずは「成功時の JSON の形を固定する」だけでも大きな価値があります。


ネスト・配列・ラッパー型のレスポンスモデル(少しステップアップ)

結果リスト+メタ情報(total_count など)を返す

実務では、

  • items: 検索結果のリスト
  • total_count: 全件数
  • limit / offset: ページング情報

のような「ラッパー JSON」を返すことがよくあります。

これもレスポンスモデルで表現できます。

from typing import List
from pydantic import BaseModel

class Item(BaseModel):
    id: int
    name: str

class ItemListResponse(BaseModel):
    total_count: int
    items: List[Item]

@app.get("/items", response_model=ItemListResponse)
def list_items():
    fake_items = [
        {"id": 1, "name": "Apple"},
        {"id": 2, "name": "Banana"},
    ]
    return {
        "total_count": 2,
        "items": fake_items
    }
Python

これで、

{
  "total_count": 2,
  "items": [
    {"id": 1, "name": "Apple"},
    {"id": 2, "name": "Banana"}
  ]
}

という形を「契約」として固定できます。

検索APIや一覧APIで、

  • 結果本体
  • 件数・ページング情報

をまとめて返したいときには、
こういう「ラッパー型レスポンスモデル」が役立ちます。


まとめ(レスポンスモデルは「外に見せる顔」をデザインするもの)

FastAPI におけるレスポンスモデルの役割を整理すると、こうなります。

レスポンスモデルは、「この API はこういう JSON を返す」という外向けの約束(契約)を、Pydantic モデルで表現したもの。
response_model=... を指定することで、余計なフィールドを自動で落とし、出してよい情報だけをクライアントに返せる(情報漏えいを防げる)。
内部モデル(UserInternal)と公開モデル(UserPublic)を分けることで、「内部の構造は自由に変えながら、外に見せる形は安定させる」設計ができる。
レスポンス側にも型バリデーションがかかるので、「返す JSON の形や型の間違い」を早期に検知でき、フロントや他サービスから使いやすい API になる。
リスト・ネスト・ラッパー型(total_count+items など)もレスポンスモデルで表現することで、検索結果や一覧、集計結果などの複雑なレスポンスも見通しよく設計できる。

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