In my FastAPI project, if I create a common header definition with Pydantic, I find that kebab-case header fields aren't behaving as expected. The "magic" conversion from kebab-case header fields in the request to their snake_case counterparts is not working, in addition to inconsistencies in the generated Swagger docs.
What is the right way to specify this Pydantic header class so that the Swagger docs and behavior match?
Here's a minimal reproduction of the problem:
### main.py
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel, Field
app = FastAPI()
class CommonHeaders(BaseModel):
simpleheader: str
a_kebab_header: str | None = Field(
default=None,
title="a-kebab-header",
alias="a-kebab-header",
description="This is a header that should be specified as `a-kebab-header`",
)
@app.get("/")
def root_endpoint(
headers: Annotated[CommonHeaders, Header()],
):
result = {"headers received": headers}
return result
If I run this and look at the Swagger docs at http://localhost:8000/docs I see this, which looks correct:
And if I "try it out" it will generate what I would expect as the correct request:
curl -X 'GET' \
'http://localhost:8000/' \
-H 'accept: application/json' \
-H 'simpleheader: foo' \
-H 'a-kebab-header: bar'
But in the response, it becomes clear it did not correctly receive the kebab-case header:
{
"headers received": {
"simpleheader": "foo",
"a-kebab-header": null
}
}
Changing the header name to snake_case "a_kebab_header" in the request does not work, either.
Updating the header definition to look like this doesn't work as expected, either. The Swagger docs and actual behavior are inconsistent.
class CommonHeaders(BaseModel):
simpleheader: str
a_kebab_header: str | None = Field(
default=None,
description="This is a header that should be specified as `a-kebab-header`",
)
Notice this now results in the Swagger docs specifying it in snake_case:
And using "try it out" results in the snake_case variant:
curl -X 'GET' \
'http://localhost:8000/' \
-H 'accept: application/json' \
-H 'simpleheader: foo' \
-H 'a_kebab_header: bar'
But SURPRISINGLY this doesn't work! The response:
{
"headers received": {
"simpleheader": "foo",
"a_kebab_header": null
}
}
But in a SURPRISE ENDING, if I manually re-write the request in kebab-case:
curl -X 'GET' \
'http://localhost:8000/' \
-H 'accept: application/json' \
-H 'simpleheader: foo' \
-H 'a-kebab-header: bar'
it finally picks up that header value via the magic translation and I get the desired results back:
{"headers received":{"simpleheader":"foo","a_kebab_header":"bar"}}
What is the right way to specify this Pydantic header class so that the Swagger docs and behavior match? If the docs are inconsistent with behavior I'm going to get hassled.
As a final thought: the following way works correctly in both the OpenAPI documentation and in the application (displaying and working as kebab-case), BUT it doesn't use Pydantic and so I lose the ability to define and use a common header structure easily across my project, and instead need to declare them individually for each endpoint:
"""Alternative version without Pydantic."""
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/")
def root_endpoint(
simpleheader: Annotated[str, Header()],
a_kebab_header: Annotated[
str | None,
Header(
title="a-kebab-header",
description="This is a header that should be specified as `a-kebab-header`",
),
] = None,
):
result = {
"headers received": {
"simpleheader": simpleheader,
"a_kebab_header": a_kebab_header,
}
}
return result
In my FastAPI project, if I create a common header definition with Pydantic, I find that kebab-case header fields aren't behaving as expected. The "magic" conversion from kebab-case header fields in the request to their snake_case counterparts is not working, in addition to inconsistencies in the generated Swagger docs.
What is the right way to specify this Pydantic header class so that the Swagger docs and behavior match?
Here's a minimal reproduction of the problem:
### main.py
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel, Field
app = FastAPI()
class CommonHeaders(BaseModel):
simpleheader: str
a_kebab_header: str | None = Field(
default=None,
title="a-kebab-header",
alias="a-kebab-header",
description="This is a header that should be specified as `a-kebab-header`",
)
@app.get("/")
def root_endpoint(
headers: Annotated[CommonHeaders, Header()],
):
result = {"headers received": headers}
return result
If I run this and look at the Swagger docs at http://localhost:8000/docs I see this, which looks correct:
And if I "try it out" it will generate what I would expect as the correct request:
curl -X 'GET' \
'http://localhost:8000/' \
-H 'accept: application/json' \
-H 'simpleheader: foo' \
-H 'a-kebab-header: bar'
But in the response, it becomes clear it did not correctly receive the kebab-case header:
{
"headers received": {
"simpleheader": "foo",
"a-kebab-header": null
}
}
Changing the header name to snake_case "a_kebab_header" in the request does not work, either.
Updating the header definition to look like this doesn't work as expected, either. The Swagger docs and actual behavior are inconsistent.
class CommonHeaders(BaseModel):
simpleheader: str
a_kebab_header: str | None = Field(
default=None,
description="This is a header that should be specified as `a-kebab-header`",
)
Notice this now results in the Swagger docs specifying it in snake_case:
And using "try it out" results in the snake_case variant:
curl -X 'GET' \
'http://localhost:8000/' \
-H 'accept: application/json' \
-H 'simpleheader: foo' \
-H 'a_kebab_header: bar'
But SURPRISINGLY this doesn't work! The response:
{
"headers received": {
"simpleheader": "foo",
"a_kebab_header": null
}
}
But in a SURPRISE ENDING, if I manually re-write the request in kebab-case:
curl -X 'GET' \
'http://localhost:8000/' \
-H 'accept: application/json' \
-H 'simpleheader: foo' \
-H 'a-kebab-header: bar'
it finally picks up that header value via the magic translation and I get the desired results back:
{"headers received":{"simpleheader":"foo","a_kebab_header":"bar"}}
What is the right way to specify this Pydantic header class so that the Swagger docs and behavior match? If the docs are inconsistent with behavior I'm going to get hassled.
As a final thought: the following way works correctly in both the OpenAPI documentation and in the application (displaying and working as kebab-case), BUT it doesn't use Pydantic and so I lose the ability to define and use a common header structure easily across my project, and instead need to declare them individually for each endpoint:
"""Alternative version without Pydantic."""
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/")
def root_endpoint(
simpleheader: Annotated[str, Header()],
a_kebab_header: Annotated[
str | None,
Header(
title="a-kebab-header",
description="This is a header that should be specified as `a-kebab-header`",
),
] = None,
):
result = {
"headers received": {
"simpleheader": simpleheader,
"a_kebab_header": a_kebab_header,
}
}
return result
Share
Improve this question
edited Feb 8 at 12:40
Helen
97.7k17 gold badges275 silver badges342 bronze badges
asked Feb 7 at 20:05
sql_knievelsql_knievel
1,4001 gold badge15 silver badges33 bronze badges
1
|
2 Answers
Reset to default 1The only way I found is to define parameters without using Pydantic model.
To use this common parameters in different endpoints you can define them using dependency function:
from typing import Annotated
from fastapi import Depends, FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
simpleheader: str
a_kebab_header: str | None
def get_common_headers(
simpleheader: Annotated[str, Header()],
a_kebab_header: str | None = Header(
default=None,
title="a-kebab-header",
alias="a-kebab-header",
description="This is a header that should be specified as `a-kebab-header`",
),
):
return CommonHeaders(simpleheader=simpleheader, a_kebab_header=a_kebab_header)
@app.get("/")
def root_endpoint(
headers: Annotated[CommonHeaders, Depends(get_common_headers)],
):
result = {"headers received": headers}
return result
@app.get("/another")
def another_endpoint(
headers: Annotated[CommonHeaders, Depends(get_common_headers)],
):
result = {"headers received": headers}
return result
Use serialization_alias
instead:
class CommonHeaders(BaseModel):
simpleheader: str
a_kebab_header: str | None = Field(
default=None,
title="a-kebab-header",
serialization_alias="a-kebab-header",
description="This is a header that should be specified as `a-kebab-header`",
)
It seems to work then but TBH I don't know why
__init__
method of model. But since alias contains hyphens it can't use this alias as a parameter name. So, it uses original name – Yurii Motov Commented Feb 8 at 6:23