FastAPI のクエリパラメータの宣言に Pydantic モデルが正式に使えるようになった | TrustHub テックブログ

2024/09/20に公開

トラストハブ AI チームの中村です。

つい先日、FastAPI から v0.115.0 がリリースされました。

https://github.com/fastapi/fastapi/releases/tag/0.115.0

これにより、FastAPI のクエリパラメータの宣言に Pydantic モデルが使えるようになりました。

本記事では、今回リリースされた新機能の具体的な使用方法を、それが特に威力を発揮できそうなユースケースに沿って解説していこうと思います。

新機能の概要

今回のリリースで、FastAPI のクエリパラメータの指定に Pydantic モデルが使えるようになりました。Pydantic モデルは強力な型ヒントとバリデーション機能を提供するので、今後はクエリパラメータの型チェック、制約条件の設定、そしてデータの正規化などが容易に実装できるようになります。また、クエリだけではなくヘッダーや Cookie の宣言も同じく Pydantic モデルが適用できるようになったので、これらも同様にバリデーションや制限を比較的簡単に実装できるようになりました。

その他のアップデート内容については、公式ドキュメントのリリースノートを参照してください。

https://fastapi.tiangolo.com/release-notes/#01150

Pydantic モデルを使用したクエリパラメータの宣言

まずは簡単な例を見ていきましょう。ここでは、とある EC サイトの商品検索 API の検索機能を FastAPI で実装するケースを考えます。

検索機能の仕様

エンドポイント

  • GET /search

クエリパラメータ

  • query * : 検索クエリの文字列(最大50文字)
  • category * : 検索する商品のカテゴリーで、以下のどれか
    • 本 (books)
    • 衣服 (clothing)
    • 電化製品 (electronics)
  • page:検索結果のページ番号(1以上)、デフォルトは 1
  • per_page:1ページあたりのアイテム数(1〜100)、デフォルトは 20

ここで * が付いているパラメータは入力必須とします。

実装例

まずは、クエリパラメータで使用する Pydantic モデル SearchParams を定義します。

from typing import Literal

from pydantic import BaseModel, Field

class SearchParams(BaseModel):
    model_config = {"extra": "forbid"}
    query: str = Field(..., min_length=1, max_length=50)
    category: Literal["books", "clothing", "electronics"]
    page: int = Field(1, gt=0)
    per_page: int = Field(20, gt=0, le=100)

ここでは、Pydantic の Field を使って、デフォルト値の設定やクエリの条件を設定しています。また、上記以外の余分なクエリパラメータの入力を許可しないために model_config{"extra": "forbid"} を追加しています。

あとは、通常通り FastAPI のエンドポイントを定義するだけですが、以下のように引数の type hint をAnnotated[SearchParams, Query()] としているところが新しい部分です。先ほど書いた Pydantic モデルと合わせた実装例が次のようになります。

main.py
from typing import Annotated, Literal

from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()

class SearchParams(BaseModel):
    model_config = {"extra": "forbid"}
    query: str = Field(..., min_length=1, max_length=50)
    category: Literal["books", "clothing", "electronics"]
    page: int = Field(1, gt=0)
    per_page: int = Field(20, gt=0, le=100)

@app.get("/search")
async def search_products(
    search_params: Annotated[SearchParams, Query()],  # 新しい部分
):
    # ここに検索ロジックを書く(今回は省略)
    return {"params": search_params}

使用例

早速、今回実装したエンドポイントにリクエストを送信してみましょう。今回は以下の実行環境を用いて、ローカルホストで開発サーバーを起動して試してみます。

実行環境

  • Python 3.12.6
  • FastAPI 0.115.0
参考:pyproject.toml
pyproject.toml
[tool.poetry]
package-mode = false
    
[tool.poetry.dependencies]
python = "^3.12.6"
fastapi = {extras = ["standard"], version = "^0.115.0"}

開発サーバー起動

fastapi dev main.py

リクエスト成功例

次のようなクエリを考えてみましょう:

  • 検索クエリ query : python
  • 商品カテゴリー category : books
curl -X 'GET' \
   "http://localhost:8000/search?query=python&category=books"

この例では、クエリパラメータの型バリデーションも制限もクリアしているので、クエリは無事に通過して正常に処理されます。

また、クエリパラメータとして pageper_page が何も指定されていないので、この場合は Pydantic モデルの Fieldで定義した通りデフォルト値が適用されます。

  • Response 200
{
  "params": {
    "query": "python",
    "category": "books",
    "page": 1,
    "per_page": 20
  }
}

型のバリデーションが通らない例

先ほどの成功例を少し変えてみます:

  • 検索クエリ query : water
  • 商品カテゴリー category : drinks
curl -X 'GET' \
   "http://localhost:8000/search?query=water&category=drinks"

この例では、商品カテゴリーに drinks が入力されており、これは先ほどの SearchParams において Literal を用いて定義した通り、想定していた入力の範囲外になっています。したがって、Pydantic モデルの型バリデーションを通過しません。

試しにリクエストを送信すると、無事にステータスコード 422 を返してくれます。

  • Response 422
{
  "detail": [
    {
      "type": "literal_error",
      "loc": [
        "query",
        "category"
      ],
      "msg": "Input should be 'books', 'clothing' or 'electronics'",
      "input": "drinks",
      "ctx": {
        "expected": "'books', 'clothing' or 'electronics'"
      }
    }
  ]
}

クエリ制限に引っかかる例

余分なクエリパラメータを追加した場合はどうなるか試してみましょう:

  • 検索クエリ query : python
  • 商品カテゴリー category : books
  • 価格の最大値 max_price : 3000
curl -X 'GET' \
   "http://localhost:8000/search?query=python&category=books&max_price=3000"

今回の例では、想定していなかったクエリパラメータ「価格の最大値 max_price」を入力しています。確かに、他のパラメータについては型のバリデーションをクリアしているはずですが、先ほどの SearchParamsmodel_config = {"extra": "forbid"}と設定したおかげで、余分なクエリパラメータの入力を受け付けないようになっています。

試しにリクエストを送信すると、無事にステータスコード 422 を返してくれます。

  • Response 422
{
  "detail": [
    {
      "type": "extra_forbidden",
      "loc": [
        "query",
        "max_price"
      ],
      "msg": "Extra inputs are not permitted",
      "input": "3000"
    }
  ]
}
コラム:旧機能のみで実装した場合

FastAPI は何といってもやはり Pydantic の強力なサポートがあってこそ、そのおかげで型安全な API 開発ができるのが強みの一つです。なので、今回のリリース以前からクエリパラメータの宣言に Pydantic モデルを使いたいユーザーは多かったのではないでしょうか。実際、自分もそのユーザーの一人でした。

次の GitHub の Discussion における実装例では、 FastAPI の Depends を用いてクエリパラメータの宣言を試しています。一見上手くいっているようには見えるのですが、Pydantic モデルの Field で定義したはずの description が OpenAPI 文書上では正常に反映されていない、という Issue が元になった Discussion です。

https://github.com/fastapi/fastapi/discussions/8634

今回、クエリパラメータでも Pydantic モデルが公式にサポートされるようになりました。上でも分かるとおり以前から GitHub の Discussion でも活発に議論されていたようなので、これは多くのユーザーにとって待望のアップデートだったのではないでしょうか。

本稿では主にクエリパラメータの例で説明しましたが、リリースノートにもあるとおり今回のリリースにはヘッダーや Cookie に対するサポートも含まれています。以下、公式のリリースノートにあるコードをそのまま掲載しただけなのですが、ヘッダーと Cookie の場合の例も紹介しようと思います。主な変更点は、単に type hint で Query() としていた部分を Header()Cookie() に変更しただけです。

ヘッダーの例

from typing import Annotated

from fastapi import FastAPI, Header
from pydantic import BaseModel

app = FastAPI()

class CommonHeaders(BaseModel):
    host: str
    save_data: bool
    if_modified_since: str | None = None
    traceparent: str | None = None
    x_tag: list[str] = []

@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):  # ヘッダーの場合
    return headers

from typing import Annotated

from fastapi import Cookie, FastAPI
from pydantic import BaseModel

app = FastAPI()

class Cookies(BaseModel):
    session_id: str
    fatebook_tracker: str | None = None
    googall_tracker: str | None = None

@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):  # Cookie の場合
    return cookies

まとめ

今回の FastAPI のリリース v0.115.0 によって、クエリ・ヘッダー・Cookie パラメータの宣言に Pydantic モデルを正式に使用できるようになりました。このアップデートは、特に複雑なパラメータを扱う API や、厳密なバリデーションが必要なケースで真価を発揮すると思われます。是非、みなさんのプロジェクトでも試してみてください。

TrustHub テックブログ

Discussion