0

Suppose I have the following hello world-ish example:

from dataclasses import dataclass
from typing import Union

from fastapi import FastAPI


@dataclass
class Item:
    name: str
    price: float
    description: Union[str, None] = None
    tax: Union[float, None] = None


app = FastAPI()


@app.post("/items/")
async def create_item(item: Item):
    return item

I want to validate the item.name with some custom logic based on the value of HTTP Host Header. Let's say I only allow names that are equal to the exact value of that header. How can I achieve that?

Perhaps the question could be rephrased to "How to let custom Pydantic validator access request data in FastAPI?".

I see that making I/O calls in Pydantic validators is generally discouraged, but for my usecase I don't plan to query anything outside my application.

blahblah
  • 1,161
  • 2
  • 14
  • 30
  • You could use use Starlette's `Request` object inside of an endpoint or a dependency function/class. Please have a look at [this answer](https://stackoverflow.com/a/74011067/17865804) and [this answer](https://stackoverflow.com/a/74015930/17865804) – Chris Jul 25 '23 at 10:21

1 Answers1

1

FastAPI doesn't allow directly using the HTTP request data in Pydantic models or validators since it violates the separation of concerns principle and couples the models to the specific request context, which should ideally be separated.

However, you can access the HTTP request in the path operation function itself. Here you can extract the Host header, perform your validation, and then pass it to your Item model if needed.

from dataclasses import dataclass
from typing import Union

from fastapi import FastAPI, HTTPException, Depends, Request
from pydantic import BaseModel, validator

class Item(BaseModel):
    name: str
    price: float
    description: Union[str, None] = None
    tax: Union[float, None] = None

    @validator("name")
    def name_must_match_header(cls, v, *, values, **kwargs):
        if 'header_value' in values and v != values['header_value']:
            raise ValueError('Name must be equal to the host header value')
        return v

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item, request: Request):
    header_value = request.headers.get('host')
    try:
        validated_item = Item(**item.dict(), header_value=header_value)
    except ValueError as ve:
        raise HTTPException(status_code=400, detail=str(ve))

    return validated_item

I have slightly refactored the Item model to be a Pydantic BaseModel instead of a dataclass, because FastAPI and Pydantic work better together when using BaseModel. I added a name_must_match_header validator in the Item class which checks if the 'name' field matches the header_value we pass when validating the model. Then, in the path operation function, I extract the 'host' header from the request and validate the Item instance using this header. If the validation fails, it raises an HTTPException with a 400 status code and the validation error as detail. If the validation passes, the function returns the validated item.

Please note that the FastAPI server automatically lowercases all header names so even if you pass Host as the header name, you'll have to retrieve it using host.

RiQts
  • 74
  • 3