404 Not Found

404 Not Found


nginx

Response Model — Precise Control of API Output

レスポンスモデルは舞台の照明のようなものです。観客に見せるべきエリアだけを照らし, 舞台裏の機材 (内部フィールド)は暗いままにしておくことで, 美しさと安全性の両方を確保します。

1. 学ぶ内容


2. Alice のリアルストーリー

(1) ペインポイント:卸売価格が競合に漏洩

Alice の PriceTracker は商品の小売価格と卸売価格を保存しています。ある日, Bob がフロントエンドから商品詳細をリクエストしたところ, API が卸売価格も返してしまいました。競合が API をスクレイピングしてすべての卸売価格情報を取得し, Alice の顧客は非常に不満になりました。問題の根本原因は, Flask にレスポンスフィルタリング機構がなく, ORM オブジェクトの全フィールドがそのままシリアライズされて返されていたことです。

(2) response_model の解決策

FastAPI の response_model 宣言的フィルタリング — モデルで宣言されたフィールドのみを返し, 未宣言のフィールドは自動的に除外し, スキーマレベルでデータ漏洩を防ぎます。

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductPublic(BaseModel):
    id: int
    name: str
    retail_price: float  # 小売価格のみ

class ProductAdmin(BaseModel):
    id: int
    name: str
    retail_price: float
    wholesale_price: float  # 卸売価格を含む

app = FastAPI()

@app.get("/products/{id}", response_model=ProductPublic)
async def get_product_public(id: int):
    # DB に wholesale_price があっても, response_model がフィルタリング
    return {"id": id, "name": "Widget", "retail_price": 29.99, "wholesale_price": 15.0}

(3) 成果

レスポンスフィルタリングは「手動でフィールドを削除する」から「宣言されたモデルに基づく自動フィルタリング」に変わり, 卸売価格漏洩の問題が完全に解消されました。公開 API は ProductPublic で宣言されたフィールドのみを返し, 管理 API は ProductAdmin で完全なデータを返します。


3. response_model の基本

(1) 自動フィルタリングの原理

100%
flowchart LR
    A[ORM オブジェクト] --> B{response_model}
    B -->|宣言されたフィールド| C[JSON レスポンス]
    B -->|未宣言のフィールド| D[フィルタリング済み]
    
    subgraph ProductPublic
        E[id]
        F[name]
        G[retail_price]
    end
    
    subgraph Hidden
        H[wholesale_price]
        I[cost_price]
        J[supplier_id]
    end

(1) ▶ サンプル:response_model による自動フィルタリング

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductResponse(BaseModel):
    id: int
    name: str
    category: str

# 追加フィールドを含む模擬データベースレコード
DB_PRODUCT = {
    "id": 1,
    "name": "Widget",
    "category": "electronics",
    "cost_price": 8.50,  # レスポンスに含めるべきではない
    "supplier_id": 42,   # レスポンスに含めるべきではない
}

app = FastAPI()

@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
    # cost_price と supplier_id は自動フィルタリングされる
    return DB_PRODUCT

出力:

TEXT
{"id": 1, "name": "Widget", "category": "electronics"}

(2) response_model と戻り値型の比較

方法 フィルタリング動作 ドキュメント生成 推奨シナリオ
response_model=X 自動フィルタリング あり 常に推奨
戻り値型 -> X フィルタリングなし あり 型アノテーションのみ
宣言なし フィルタリングなし なし 非推奨

(2) ▶ サンプル:response_model と戻り値型の比較

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

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

app = FastAPI()

@app.get("/demo/model", response_model=ProductBrief)
async def with_response_model():
    # response_model はフィルタリング:レスポンスには id と name のみ
    return {"id": 1, "name": "Widget", "secret": "hidden"}

@app.get("/demo/type") -> ProductBrief
async def with_return_type():
    # 戻り値型はフィルタリングしない:secret フィールドが漏洩!
    return {"id": 1, "name": "Widget", "secret": "leaked"}

出力:

TEXT
# 関数定義成功

4. フィールドレベルのきめ細かいフィルタリング

(1) exclude と include

すべてのシナリオで新しいモデルを作成したくない場合, response_model_excluderesponse_model_include を使用して動的にフィールドをフィルタリングできます。

(1) ▶ サンプル:exclude で機密フィールドを除外

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field

class ProductFull(BaseModel):
    id: int
    name: str
    retail_price: float
    wholesale_price: float = Field(exclude=True)  # デフォルトで除外
    cost_price: float = Field(exclude=True)
    supplier: str = Field(exclude=True)

app = FastAPI()

@app.get("/products/{id}", response_model=ProductFull)
async def get_product(id: int):
    return {
        "id": id,
        "name": "Widget",
        "retail_price": 29.99,
        "wholesale_price": 15.0,
        "cost_price": 8.50,
        "supplier": "Acme Corp",
    }

出力:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

出力 (wholesale_price, cost_price, supplier はフィルタリング済み):

TEXT
{"id": 1, "name": "Widget", "retail_price": 29.99}

(2) ▶ サンプル:response_model_exclude 動的除外

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductFull(BaseModel):
    id: int
    name: str
    retail_price: float
    wholesale_price: float
    cost_price: float

app = FastAPI()

@app.get(
    "/products/{id}",
    response_model=ProductFull,
    response_model_exclude={"wholesale_price", "cost_price"},
)
async def get_product_public(id: int):
    return {
        "id": id, "name": "Widget",
        "retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
    }

@app.get(
    "/admin/products/{id}",
    response_model=ProductFull,
)
async def get_product_admin(id: int):
    return {
        "id": id, "name": "Widget",
        "retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
    }

出力:

TEXT
# 関数定義成功
フィルタリング方法 対象シナリオ 粒度
response_model=ModelA 異なる視点からの異なるモデル モデルレベル
response_model_exclude={fields} 少数のフィールドを除外 フィールドレベル
response_model_include={fields} 少数のフィールドのみ含める フィールドレベル
Field(exclude=True) 特定のフィールドを除外 フィールド定義レベル

5. マルチレスポンスモデルと高度なテクニック

(1) 同じエンドポイントで異なるレスポンス

(1) ▶ サンプル:Union レスポンスモデル

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union

class ProductFound(BaseModel):
    id: int
    name: str
    price: float

class ProductNotFound(BaseModel):
    error: str
    product_id: int

app = FastAPI()

@app.get("/products/{id}", response_model=Union[ProductFound, ProductNotFound])
async def search_product(id: int):
    if id == 1:
        return ProductFound(id=1, name="Widget", price=9.99)
    return ProductNotFound(error="Not found", product_id=id)

出力:

TEXT
# 関数定義成功

(2) ▶ サンプル:リストレスポンスモデル

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class PriceEntry(BaseModel):
    product_id: int
    price: float
    recorded_at: str

app = FastAPI()

@app.get("/products/{id}/prices", response_model=list[PriceEntry])
async def get_price_history(id: int):
    return [
        {"product_id": id, "price": 29.99, "recorded_at": "2026-01-01"},
        {"product_id": id, "price": 24.99, "recorded_at": "2026-02-01"},
        {"product_id": id, "price": 34.99, "recorded_at": "2026-03-01"},
    ]

出力:

TEXT
# 関数定義成功

(2) exclude_unset と exclude_none

(3) ▶ サンプル:exclude_unset で値のあるフィールドのみ返す

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

class ProductUpdate(BaseModel):
    name: Optional[str] = None
    category: Optional[str] = None
    price: Optional[float] = None

app = FastAPI()

@app.get(
    "/products/{id}",
    response_model=ProductUpdate,
    response_model_exclude_unset=True,
)
async def get_product_partial(id: int):
    # name のみ設定され, category と price は None のまま
    return ProductUpdate(name="Updated Widget")

出力:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

出力 (値のあるフィールドのみ含む):

TEXT
{"name": "Updated Widget"}
オプション 効果 ユースケース
response_model_exclude_unset=True 未設定のフィールドを除外 PATCH レスポンス:更新されたフィールドのみ返す
response_model_exclude_none=True 値が None のフィールドを除外 null 値を削除してレスポンスを簡潔に
response_model_exclude_defaults=True デフォルト値を使用するフィールドを除外 冗長なデフォルト値を削除
response_model_by_alias=True エイリアスを使用してフィールドを出力 フロントエンドが camelCase 命名を要求

(4) ▶ サンプル:response_model_by_alias

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict

class ProductResponse(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    id: int
    product_name: str = Field(alias="productName")
    unit_price: float = Field(alias="unitPrice", description="Price in USD")

app = FastAPI()

@app.get("/products/{id}", response_model=ProductResponse, response_model_by_alias=True)
async def get_product(id: int):
    return {"id": id, "productName": "Widget", "unitPrice": 9.99}

出力:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

出力 (エイリアスをフィールド名に使用):

TEXT
{"id": 1, "productName": "Widget", "unitPrice": 9.99}

❓ よくある質問

Q response_model と戻り値型アノテーションの違いは何ですか?
A response_model は未宣言のフィールドをフィルタリングし, 出力をバリデーションします。戻り値型アノテーションは OpenAPI ドキュメント生成にのみ影響し, フィールドのフィルタリングは行いません。常に response_model を使用してください。
Q 「exclude」と「include」は同時に使用できますか?
A いいえ, 同時には使用できません。どちらかを選択してください:「exclude」は指定したフィールドを除外し, 「include」は指定したフィールドのみを含めます。
Q exclude_unset はどのようなシナリオで最も役立ちますか?
A PATCH リクエストへのレスポンスです。クライアントが一部のフィールドのみ更新する場合, レスポンスは更新されたフィールドのみを返すことで混乱を防ぎます。
Q ネストされたモデル内のフィールドを除外できますか?
A はい, ドット区切りのパス {"nested_model": {"secret_field"}} を使用してネストされたフィールドを除外できます。
Q リストレスポンスはどのように宣言しますか?
A response_model=list[ItemModel] を使用します。FastAPI はリスト内の各要素をモデルに従って自動的にフィルタリングします。
Q response_model はパフォーマンスに影響しますか?
A わずかなオーバーヘッド (シリアライズ + フィルタリング)がありますが, データセキュリティとドキュメントの一貫性が確保されます。数百万 QPS のシナリオでは orjson でシリアライズを最適化できます。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):ProductPublic モデル (id, name, price)を作成し, response_model を使用してエンドポイントがこの3つのフィールドのみを返すようにしてください。cost_price を含む dict を返しても情報は漏洩しません。ヒント:@app.get(..., response_model=ProductPublic)
  2. 応用問題 (難易度 ⭐⭐):PriceTracker の2つのエンドポイントを作成してください — wholesale_price を非表示にする公開エンドポイントと, 全フィールドを表示する管理者エンドポイントです。response_model_exclude で実装してください。ヒント:response_model_exclude={"wholesale_price"}
  3. チャレンジ問題 (難易度 ⭐⭐⭐):オプショナルフィールド (description, image_url など)を持つ ProductDetail モデルを作成し, response_model_exclude_unset=True を使用して, エンドポイントがクライアントから提供されたフィールドのみを返し, 空の値が JSON から省略されるようにしてください。ヒント:Optional[str] = None + response_model_exclude_unset=True

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%