CodeSampleX

サンプル

fastapi 0.141.1: Bind query parameters to Pydantic models using Annotated and Query without silent multi-value dropping or request body coercion

検証済みサンプル — pypi fastapi 0.141.1: Bind query parameters to Pydantic models using Annotated and Query without silent multi-value dropping or request body…

sha256:e332c164b1327606b7ddbb7dca07956830a4f2bf723ca9a1926291a42cfb0a88

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。 MIT-0

実行証拠

宣言された環境と署名済みの実行を分けてあります。このサンプルが何をどこで実行したかをそのまま確認できます。

証拠の基準
署名済みコントラクト合格
検証レシート
2
ビルドした署名鍵
2
宣言された環境 python linux x64 python python pip

検証実行環境

環境 コントラクト ステージ 実行日
python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-17
python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-18

ケース

HOW
ゴール
Bind query parameters to Pydantic models using Annotated and Query without silent multi-value dropping or request body coercion
パッケージ
シンボル
  • fastapi.FastAPI
  • fastapi.Query
  • fastapi.Depends
  • fastapi.testclient.TestClient
  • pydantic.BaseModel
  • pydantic.Field
  • pydantic.ConfigDict
環境
python
作成日
2026-08-17T03:19:11Z

コントラクト

  1. assert unannotated Pydantic model parameter in GET route is treated as request body and returns 422 for query parameters
  2. assert Depends(Model) query parameter workaround silently drops repeated query parameters into empty list with 200 OK
  3. assert Annotated[Model, Query()] binds query parameters preserving repeated parameters into list fields
  4. assert Annotated[Model, Query()] resolves field aliases from query parameters
  5. assert Annotated[Model, Query()] with extra='forbid' rejects unrecognized query parameters with 422 and query loc
  6. assert scalar Query parameter with multiple values in query string binds to the last value

ファイル

  • NOTES.md
  • csx.json
  • requirements.txt
  • src/__init__.py
  • src/app.py
  • test/contract.py

ソースアーティファクトをダウンロード (tar.gz)

ソース

NOTES.md
# FastAPI Query Parameter Model Binding and Version Boundary Traps

## CSX Search Result
`search_known_solution` returned COMPATIBLE alternatives for unrelated packages (`pkg:pypi/packaging` SpecifierSet evaluation in `sha256:2158f71f8f0182e574ebf8c6a651b8c00cc0e084fe82523db391f8d026016a3c`, `pkg:pypi/importlib-metadata` distribution-vs-import names in `sha256:beb8b3121c3cddc83a0038650f092bd1609123c82e31e1209dc60f07e688d56f`, and `pkg:npm/esbuild` binary platform dependencies in `sha256:a882cd491a5e648e2d40804b1d5c8f37c85ca95d73a0ff6fada45656d19805d3`), with no existing sample covering FastAPI's query parameter model binding boundaries.

## What a Model Would Have Written Instead
A model would either declare `filter: FilterParams` directly expecting FastAPI to bind GET query parameters to the model, or use the pre-0.115 workaround `filter: Annotated[FilterParams, Depends()]` to group query parameters into a Pydantic model.

## How the Wrong Version Fails
Declaring `filter: FilterParams` fails loudly with HTTP 422 (`loc: ["body"]`) on GET requests because unannotated Pydantic models default to request bodies, while the pre-0.115 `Depends()` workaround fails SILENTLY with a green HTTP 200 build where multi-value query parameters (`?tags=a&tags=b`) are discarded into an empty list `[]`.
csx.json
{"case":{"caseId":"case:sha256:97c71e5ffb29deed64c33d987ad647450b528eb3c3b470c67026b7108c1adfd8","contract":["assert unannotated Pydantic model parameter in GET route is treated as request body and returns 422 for query parameters","assert Depends(Model) query parameter workaround silently drops repeated query parameters into empty list with 200 OK","assert Annotated[Model, Query()] binds query parameters preserving repeated parameters into list fields","assert Annotated[Model, Query()] resolves field aliases from query parameters","assert Annotated[Model, Query()] with extra='forbid' rejects unrecognized query parameters with 422 and query loc","assert scalar Query parameter with multiple values in query string binds to the last value"],"goal":"Bind query parameters to Pydantic models using Annotated and Query without silent multi-value dropping or request body coercion","kind":"HOW","packages":["pkg:pypi/fastapi@0.141.1","pkg:pypi/pydantic@2.13.4","pkg:pypi/starlette@1.6.0","pkg:pypi/httpx@0.28.1"],"schemaVersion":1,"symbols":["fastapi.FastAPI","fastapi.Query","fastapi.Depends","fastapi.testclient.TestClient","pydantic.BaseModel","pydantic.Field","pydantic.ConfigDict"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/fastapi@0.141.1","pkg:pypi/pydantic@2.13.4","pkg:pypi/starlette@1.6.0","pkg:pypi/httpx@0.28.1"],"schemaVersion":1,"symbols":["fastapi.FastAPI","fastapi.Query","fastapi.Depends","fastapi.testclient.TestClient","pydantic.BaseModel","pydantic.Field","pydantic.ConfigDict"],"verifierAdapter":"python@1"}
requirements.txt
fastapi==0.141.1
starlette==1.6.0
annotated-doc==0.0.5
pydantic==2.13.4
pydantic-core==2.46.4
typing-extensions==4.16.0
typing-inspection==0.4.4
annotated-types==0.8.0
anyio==4.14.2
idna==3.18
httpx==0.28.1
httpcore==1.0.9
h11==0.16.0
certifi==2026.7.22
src/__init__.py
"""Package initialization."""
src/app.py
"""FastAPI application demonstrating query parameter binding semantics and traps."""

from typing import Annotated, Optional
from fastapi import FastAPI, Query, Depends
from pydantic import BaseModel, Field, ConfigDict


class FilterParams(BaseModel):
    """Standard filter parameter container model."""

    page: int = 1
    limit: int = 10
    tags: list[str] = []
    sort_by: Optional[str] = Field(default=None, alias="sortBy")


class StrictFilterParams(BaseModel):
    """Filter parameter container forbidding unexpected query parameters."""

    model_config = ConfigDict(extra="forbid")

    page: int = 1
    query: Optional[str] = None


app = FastAPI(title="FastAPI Query Parameter Binding Traps")


@app.get("/items/unannotated")
def get_items_unannotated(filter: FilterParams):
    """Unannotated Pydantic parameter trap: coerced to JSON request body, not query params."""
    return filter.model_dump()


@app.get("/items/depends")
def get_items_depends(filter: Annotated[FilterParams, Depends()]):
    """Pre-0.115 workaround trap: 200 OK but silently drops repeated query parameters."""
    return filter.model_dump()


@app.get("/items/query")
def get_items_query(filter: Annotated[FilterParams, Query()]):
    """FastAPI 0.115+ model query binding: correctly captures repeated parameters and aliases."""
    return filter.model_dump()


@app.get("/items/strict")
def get_items_strict(filter: Annotated[StrictFilterParams, Query()]):
    """Strict model query binding: rejects unexpected query parameters with HTTP 422."""
    return filter.model_dump()


@app.get("/items/scalar")
def get_items_scalar(tag: str = Query(default="")):
    """Scalar query parameter: when duplicated in query string, last value wins."""
    return {"tag": tag}
test/contract.py
"""Contract test verifying FastAPI query parameter model binding semantics and traps."""

import os
import sys

# Ensure project root is in sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from fastapi.testclient import TestClient
from src.app import app


def test_unannotated_model_treated_as_body():
    """Unannotated Pydantic parameter is interpreted as JSON Request Body, not Query parameters."""
    client = TestClient(app)
    response = client.get("/items/unannotated?page=1&limit=10")
    # Naive model expectation: HTTP 200 with parsed query parameters
    # FastAPI reality: HTTP 422 because GET route expects a JSON request body
    assert response.status_code == 422, (
        f"Expected HTTP 422 for unannotated model parameter, got {response.status_code}"
    )
    detail = response.json().get("detail", [])
    assert len(detail) > 0
    assert detail[0].get("loc", [])[0] == "body", (
        f"Expected error location 'body', got {detail[0].get('loc')}"
    )


def test_depends_workaround_silently_drops_repeated_params():
    """Pre-0.115 Depends(Model) workaround returns 200 OK but silently drops repeated query parameters."""
    client = TestClient(app)
    response = client.get(
        "/items/depends?page=2&tags=python&tags=fastapi&sortBy=created_at"
    )
    assert response.status_code == 200
    data = response.json()
    assert data["page"] == 2
    assert data["sort_by"] == "created_at"
    # Naive model expectation: tags == ['python', 'fastapi']
    # FastAPI reality: Depends() treats model fields as endpoint dependencies,
    # silently failing to collect repeated query parameters into list, leaving it empty []
    assert data["tags"] == [], (
        f"Expected tags to be silently dropped into empty list in Depends(), got {data['tags']}"
    )


def test_annotated_query_model_preserves_repeated_params():
    """FastAPI 0.115+ Annotated[Model, Query()] binds query parameters and collects repeated keys."""
    client = TestClient(app)
    response = client.get(
        "/items/query?page=2&tags=python&tags=fastapi&sortBy=created_at"
    )
    assert response.status_code == 200
    data = response.json()
    assert data["page"] == 2
    assert data["sort_by"] == "created_at"
    assert data["tags"] == ["python", "fastapi"], (
        f"Expected parsed query list ['python', 'fastapi'], got {data['tags']}"
    )


def test_annotated_query_model_resolves_field_aliases():
    """Annotated[Model, Query()] resolves field aliases from query parameters."""
    client = TestClient(app)
    response = client.get("/items/query?sortBy=name")
    assert response.status_code == 200
    assert response.json()["sort_by"] == "name"


def test_strict_query_model_forbids_extra_params():
    """Annotated[Model, Query()] with extra='forbid' rejects unrecognized query parameters."""
    client = TestClient(app)
    ok_response = client.get("/items/strict?page=3&query=search_term")
    assert ok_response.status_code == 200
    assert ok_response.json() == {"page": 3, "query": "search_term"}

    err_response = client.get("/items/strict?page=3&unexpected_param=evil")
    assert err_response.status_code == 422
    loc = err_response.json()["detail"][0]["loc"]
    assert loc == ["query", "unexpected_param"], (
        f"Expected validation error at ['query', 'unexpected_param'], got {loc}"
    )


def test_scalar_query_parameter_last_value_precedence():
    """Scalar Query parameter with multiple values in query string binds to the last value."""
    client = TestClient(app)
    response = client.get("/items/scalar?tag=first&tag=second")
    assert response.status_code == 200
    assert response.json() == {"tag": "second"}


if __name__ == "__main__":
    test_unannotated_model_treated_as_body()
    test_depends_workaround_silently_drops_repeated_params()
    test_annotated_query_model_preserves_repeated_params()
    test_annotated_query_model_resolves_field_aliases()
    test_strict_query_model_forbids_extra_params()
    test_scalar_query_parameter_last_value_precedence()
    print("CONTRACT PASS: FastAPI query parameter model binding semantics verified")

オリジンシーダー

csx-seed