2

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?

Chris
  • 18,724
  • 6
  • 46
  • 80
Amin Ba
  • 1,603
  • 1
  • 13
  • 38
  • 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. – Daniil Fajnberg Apr 12 '23 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 – Amin Ba Apr 12 '23 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. – Daniil Fajnberg Apr 12 '23 at 18:39
  • what if I wanted to do some validation on the string? how would I do this? – Amin Ba Apr 12 '23 at 18:44

1 Answers1

6

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

I would also like to mention 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 the 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(..., 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
Chris
  • 18,724
  • 6
  • 46
  • 80
  • can you read the comment on the original question? – Amin Ba Apr 12 '23 at 18:47
  • and could I include Path here as well and run complex validation between the fields? – Amin Ba Apr 12 '23 at 18:49
  • suppose I wanted a certain combination of status and comments. how could I do the validation? – Amin Ba Apr 12 '23 at 18:55
  • thanks. you are right. actually I have already taken two courses on udemy about fastapi but still I sometimes dont know what a developer is supposed to do and not supposed to do. I can create endpoint but cant understand is this what professionals are doing or not and is this sth the creators of fastapi would imagine a developer doing or not – Amin Ba Apr 12 '23 at 19:04
  • 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 – Amin Ba Apr 12 '23 at 19:05
  • if we had `services/{service_id}`, would you add `service_id: str = Field (Path(..., description=''))` under`class ServiceStatus(BaseModel)` as well or not? – Amin Ba Apr 12 '23 at 21:58
  • Please have a look at [this answer](https://stackoverflow.com/a/72068032/17865804), as well as [this answer](https://stackoverflow.com/a/74498663/17865804) and [this answer](https://stackoverflow.com/a/74536449/17865804) (you may also find [this](https://stackoverflow.com/a/72815364/17865804) helpful as well). Please make sure to read the linked answers above, as well as any references included, thoroughly. – Chris Apr 13 '23 at 05:33
  • What if I wanted to make the status choices case-insensitive? – Amin Ba Apr 25 '23 at 17:51
  • To future readers: To make `enum` values case insensitive, please have a look at [this answer](https://stackoverflow.com/a/76131490/17865804) – Chris Apr 29 '23 at 04:10