In the last post, Cookie Parameter Models, we bundled a few cookies into one Pydantic model. As promised, headers play by the same rules. If a request carries several related headers, group them into a model instead of listing each one in the signature. 🤔
Bundling headers into a model
Put the headers in a Pydantic model, then declare the parameter with Header().
from typing import Annotated, Optionalfrom fastapi import FastAPI, Headerfrom pydantic import BaseModelapp = FastAPI()class CommonHeaders(BaseModel): user_agent: str save_data: bool = False x_token: Optional[str] = None@app.get("/blog")async def read_blog(headers: Annotated[CommonHeaders, Header()]): return headersLet's review:
CommonHeadersholds all three headers in one place, ready to reuse on any endpoint.Annotated[CommonHeaders, Header()]tells FastAPI to pull each field from the request headers.The underscore-to-hyphen rule still applies, so
save_datareads the realsave-dataheader. Same convenience as plain header params.
This needs FastAPI 0.115.0 or newer, the same release that added query and cookie models.
Forbidding extra headers
Want to reject any header you did not declare? Forbid extras with Pydantic config.
class CommonHeaders(BaseModel): model_config = {"extra": "forbid"} user_agent: str save_data: bool = False x_token: Optional[str] = NoneNow a request carrying an undeclared header gets a clean error instead of being silently ignored. One important catch with headers: browsers and clients always send standard headers like host and accept, and forbid will reject those too. So this is rarely practical for real header traffic. It is far more useful with cookie and query models, where you control the full set. Reach for it on headers only when you truly know every header coming in.
Turning off the underscore conversion
If you genuinely need a header to keep its underscore, switch the conversion off on the Header() itself.
@app.get("/blog")async def read_blog( headers: Annotated[CommonHeaders, Header(convert_underscores=False)],): return headersGotcha: that convert_underscores=False flag lives on the Header() in the signature, not inside the model. And as we noted with plain headers, many proxies and servers reject headers that actually contain underscores, so leave the conversion on unless you have a specific reason. 😁
