11

I am trying to have an endpoint like /services?status=New

status is going to be either New or Old

Here is my code:

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from enum import Enum

router = APIRouter()

class ServiceStatusEnum(str, Enum):
    new = "New"
    old = "Old"


class ServiceStatusQueryParam(BaseModel):
    status: ServiceStatusEnum


@router.get("/services")
def get_services(
  status: ServiceStatusQueryParam = Query(..., title="Services", description="my desc"),
):
    pass #my code for handling this route.....

The result is that I get an error that seems to be relevant to this issue here

The error says AssertionError: Param: status can only be a request body, using Body()


Then I found another solution explained here.

So, my code will be like this:

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from enum import Enum

router = APIRouter()

class ServiceStatusEnum(str, Enum):
    new = "New"
    old = "Old"


class ServicesQueryParam(BaseModel):
    status: ServiceStatusEnum


@router.get("/services")
def get_services(
  q: ServicesQueryParam = Depends(),
):
    pass #my code for handling this route.....

It is working (and I don't understand why) - but the question is how and where do I add the description and title?

4
  • 1
    I don't understand the purpose of ServiceStatusQueryParam. Why not just annotate the status parameter of your route directly with ServiceStatusEnum. That will work. Do you really need an entire JSON object in a URL query parameter? Seems super cringe to me. Commented Apr 12, 2023 at 18:27
  • I dont clearly understand the difference bc I am new to fastapi but you seem right. I tested and it is working Commented Apr 12, 2023 at 18:35
  • 2
    The way you had it in your first code snippet, the URL query parameter would theoretically have to be ?status={"status":"New"} or something to that effect because you set the type of the status query parameter to be your ServiceStatusQueryParam model, which in turn serializes to a JSON object. Whereas you just want your query to be ?status=New, so essentially of type string, but constrained to the enum members. Commented Apr 12, 2023 at 18:39
  • what if I wanted to do some validation on the string? how would I do this? Commented Apr 12, 2023 at 18:44

1 Answer 1

25

To create a Pydantic model and use it to define query parameters, you would need to use Depends() along with the parameter in your endpoint. To add description, title, etc., to query parameters, you could wrap the Query() in a Field().

It should also be noted that one could use the Literal type instead of Enum, as described here and here. Additionally, if one would like to define a List field inside a Pydantic model and use it as a query parameter, they would either need to implement this in a separate dependency class, as demonstrated here and here, or again wrap the Query() in a Field(), as shown below.

Moreover, to perform validation on query parameters inside a Pydnatic model, one could do this as usual using Pydantic's @validator, as demonstrated here, as well as here and here. Note that in this case, where the BaseModel is used for query parameters, raising ValueError would cause an Internal Server Error. Hence, you should instead raise an HTTPException when a validation fails, or use a custom exception handler, in order to handle ValueError exceptions, as shown in Option 2 of this answer. Besides @validator, one could also have additional validations for Query parameters, as described in FastAPI's documentation (see Query class implementation as well).

As a side note, regarding defining optional parameters, the example below uses the Optional type hint (accompanied by None as the default value in Query) from the typing module; however, you may also would like to have a look at this answer and this answer, which describe all the available ways on how to do that.

Working Example

from fastapi import FastAPI, Depends, Query, HTTPException
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Literal
from enum import Enum

app = FastAPI()

class Status(str, Enum):
    new = 'New'
    old = 'Old'


class ServiceStatus(BaseModel):
    status: Optional[Status] = Field (Query(None, description='Select service status'))
    msg: Optional[str] = Field (Query(None, description='Type something'))
    choice: Literal['a', 'b', 'c', 'd'] = Field (Query(..., description='Choose something'))
    comments: List[str] = Field (Query(..., description='Add some comments'))
    
    @validator('choice')
    def check_choice(cls, v):
        if v == 'b': 
             raise HTTPException(status_code=422, detail='Wrong choice')
        return v

@app.get('/status')
def main(status: ServiceStatus = Depends()):
    return status

Update 1 (regarding @validator)

Please note that in Pydantic V2, @validator has been deprecated and replaced by @field_validator. Please have a look at this answer for more details and examples.

Update 2 (regarding Query parameters)

As of FastAPI 0.115.0, one can declare Query (as well as Header and Cookie) parameters with Pydantic models in the following way:

from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()


class FilterParams(BaseModel):
    limit: int = Field(100, gt=0, le=100)
    offset: int = Field(0, ge=0)
    order_by: Literal["created_at", "updated_at"] = "created_at"
    tags: list[str] = []


@app.get("/items")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
    return filter_query
Sign up to request clarification or add additional context in comments.

7 Comments

for example, now that you provided this example, I am wondering if I should include Path and Body in the ServiceStatus class as well or not
if we had services/{service_id}, would you add service_id: str = Field (Path(..., description='')) underclass ServiceStatus(BaseModel) as well or not?
Please have a look at this answer, as well as this answer and this answer (you may also find this helpful as well). Please make sure to read the linked answers above, as well as any references included, thoroughly.
What if I wanted to make the status choices case-insensitive?
Omg this has wasted so much of my time... Thought it was a much more complicated bug and couldn't figure it out. Thanks for posting this!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.