最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

python - Problem with FastAPI, Pydantic, and kebab-case header fields - Stack Overflow

programmeradmin0浏览0评论

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
  • 1 Here is an explanation of why it doesn't work: github.com/fastapi/fastapi/issues/12402#issuecomment-2520205504 . Pydantic uses field's alias to create signature of __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
Add a comment  | 

2 Answers 2

Reset to default 1

The 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

发布评论

评论列表(0)

  1. 暂无评论