FastAPI released 0.146.0, 0.147.0, and 0.148.0 between Sunday afternoon and Monday morning (UTC), and each one is labeled a breaking change. None of them will crash your app on startup. They change what values your endpoints receive, which is the kind of change that passes CI and then shows up as odd data a week later.
At Scout Monitoring, we monitor a lot of FastAPI apps, and a loosely pinned fastapi dependency is the most common way these releases will arrive. This post walks through each change, who it affects, and what to search your codebase for before you upgrade.
Why Three Breaking Releases Landed at Once
FastAPI is still below 1.0, so minor version bumps are allowed to break things. The FastAPI docs recommend pinning to a minor version for exactly this reason, for example fastapi>=0.145.0,<0.146.0. If your requirements say fastapi>=0.100 or just fastapi, your next build picks up all three changes together.
The three releases share a theme: making request parameter handling behave consistently. Two are bug fixes that change observable behavior, and one is a deprecation. Here they are in release order.
0.146.0 Deprecates root_path in FastAPI()
Passing root_path to the app constructor now emits a FastAPIDeprecationWarning:
# Deprecated in 0.146.0
app = FastAPI(root_path="/api/v1")
The FastAPI team says the parameter’s behavior was not consistent with the server configuration. Instead, set it on the server:
fastapi run --root-path /api/v1
# or, if you run Uvicorn directly
uvicorn main:app --root-path /api/v1
This is the gentlest of the three, since the parameter still works. The catch is test suites that turn warnings into errors. If your pytest config has filterwarnings = error, every test that builds the app will fail after the upgrade. TestClient(app, root_path="/api/v1") is unaffected and is still the documented way to set it in tests.
0.147.0 Treats Omitted Boolean Form Fields as False
Browsers don’t send unchecked checkboxes at all, so a form with an unchecked “subscribe” box arrives without a subscribe field. FastAPI now reads a missing bool or bool | None form field as False. That matches HTML, but it overrides whatever your signature says:
@app.post("/settings")
def update_settings(notify: Annotated[bool, Form()] = True):
...
Before 0.147.0, a request without notify got True. Now it gets False. Required boolean form fields that used to return a 422 when missing now succeed with False, and bool | None fields can no longer tell “not sent” apart from “unchecked”.
Browser forms are fine. The risk is API clients that post form data and only send the fields they want to change, such as a mobile app or an internal service doing partial updates. After the upgrade, every boolean those clients leave out gets written as False. JSON bodies and direct Pydantic validation are unchanged, so this only applies to Form parameters and form models.
0.148.0 Stops Validating Defaults
This is the change most likely to slip through. For omitted Query, Header, Cookie, and Form parameters, FastAPI no longer runs validation on the default value. The default is passed to your function as written. That lines up with Pydantic’s own behavior, where defaults are trusted unless you set validate_default=True.
A validator that only checks values won’t notice, but one that transforms them will. Consider a query parameter that accepts a comma-separated list and has a string default:
def split_csv(value):
return value.split(",") if isinstance(value, str) else value
@app.get("/items")
def list_items(
fields: Annotated[list[str], BeforeValidator(split_csv), Query()] = "id,name",
):
...
Before 0.148.0, an omitted fields was validated and arrived as ["id", "name"]. Now it arrives as the string "id,name", and any code that iterates over it gets characters instead of field names. The same applies to defaults that a validator was quietly rejecting: a default that violates gt=0 used to produce a 422 and is now accepted.
To fix it, opt back in wherever you depend on it:
fields: Annotated[
list[str], BeforeValidator(split_csv), Query(validate_default=True)
] = "id,name"
Use Field(validate_default=True) for fields on parameter models. Body(validate_default=True) was previously ignored and is now honored too.
The Smaller 0.148.0 Changes That Affect Tests and Partial Updates
The same release fixes three related details. They are less dramatic, but they are the ones that break assertions.
For parameter models, applying a default no longer marks a field as explicitly set. If you use model_dump(exclude_unset=True) on a Query or Form model to build a partial update, defaulted fields used to appear in the output and now don’t. Omitted booleans from 0.147.0 are the exception: they still count as set, so a form model will dump booleans as False and leave other defaulted fields out.
Default factories now run once per request. If a default_factory generates a UUID or a timestamp and you saw it called more than once before, any code or test that depended on that will see one call. Validation error responses also no longer include injected defaults in the input field, so tests that compare full 422 response bodies may need updating.
What to Search for Before You Upgrade
Most apps will be unaffected by most of this. A quick search tells you which parts apply to you:
grep -rn "FastAPI(.*root_path" .
grep -rn "bool.*Form(" .
grep -rn "default_factory" .
grep -rn "exclude_unset=True" .
grep -rnE "BeforeValidator|AfterValidator|field_validator" .
For each validator hit, check whether the parameter has a default and whether the validator changes the value. Those are the places to add validate_default=True. For each boolean form field, check whether anything other than a browser posts to that endpoint.
Then pin the version. Upgrading one minor release at a time, with the release notes open, is much easier than finding three behavior changes at once.
Catching Silent Behavior Changes in Production
None of these changes raise exceptions, which is what makes them hard to spot. What changes is the data: fewer 422s where defaults used to be rejected, booleans flipping to False on partial updates, and responses that look different for requests that omit parameters.
Scout Monitoring names FastAPI endpoints after their route functions, so moving root_path to the server won’t rename anything in your dashboards. With deploys marked on your charts, a shift in error rate or response time on a single endpoint right after the upgrade points straight at the code path to check. Error groups show the requests that started failing, along with the (filtered) parameters they were sent with.
Upgrading FastAPI this week? Start monitoring with Scout for free and see how each endpoint behaves before and after the change.