26

Is there a way to convert a pydantic model to query parameters in fastapi?

Some of my endpoints pass parameters via the body, but some others pass them directly in the query. All this endpoints share the same data model, for example:

class Model(BaseModel):
    x: str
    y: str

I would like to avoid duplicating my definition of this model in the definition of my "query-parameters endpoints", like for example test_query in this code:

class Model(BaseModel):
    x: str
    y: str

@app.post("/test-body")
def test_body(model: Model): pass

@app.post("/test-query-params")
def test_query(x: str, y: str): pass

What's the cleanest way of doing this?

cglacet
  • 8,873
  • 4
  • 45
  • 60
  • 1
    Please have a look at [this answer](https://stackoverflow.com/a/75998823/17865804), which provides a working example demonstrating how to use Pydantic models to define query parameters (including `List` query params), as well as how to validate the parameters and raise exceptions. – Chris Apr 13 '23 at 13:19

5 Answers5

29

The documentation gives a shortcut to avoid this kind of repetitions. In this case, it would give:

from fastapi import Depends

@app.post("/test-query-params")
def test_query(model: Model = Depends()): pass

This will allow you to request /test-query-params?x=1&y=2 and will also produce the correct OpenAPI description for this endpoint.

Similar solutions can be used for using Pydantic models as form-data descriptors.

JPG
  • 82,442
  • 19
  • 127
  • 206
cglacet
  • 8,873
  • 4
  • 45
  • 60
  • 1
    Wooow, thank you SO much! This has eluded me for months! It's unintuitive that `model: Model` is recognized as body params, but `model: Model = Depends()` is recognized as query params. The fastapi docs are nice and all, but sometimes they fail to highlight key things like this. – Mike B May 04 '21 at 05:54
  • @mblakesley don't hesitate asking for help on the dedicated gitter, I think that's how I got this information (https://gitter.im/tiangolo/fastapi) – cglacet May 07 '21 at 19:07
  • This is a very nice explanation. I just want to add that this goes only one level deep. If you have some_other_key: SomeOtherModel in your Model class it will be evaluated as a body parameter. So, when using this technique, only your model should inherit from parent classes, not your Model attributes. – tacan Nov 25 '22 at 18:40
  • Note that this does not work for models using "advanced" validators like `field_validator`. If validation fails in it, it will crash the call. – Finch_Powers Aug 14 '23 at 20:00
  • Seems to not work anymore with pydantic 2. The request fails asking for a body payload – Michael Pacheco Aug 15 '23 at 19:52
10

Special case that isn't mentioned in the documentation for Query Parameters Lists, for example with:

/members?member_ids=1&member_ids=2

The answer provided by @cglacet will unfortunately ignore the array for such a model:

class Model(BaseModel):
    member_ids: List[str]

You need to modify your model like so:

 class Model(BaseModel):
     member_ids: List[str] = Field(Query([]))

Answer from @fnep on GitHub here

Anthony
  • 119
  • 2
  • 4
  • 1
    Thanks man, you saved my code. I was going to delete whatever I wrote because I couldn't access list in query params with Pydantic model. – Sumeet Apr 28 '23 at 17:06
2

This solution is very apt if your schema is "minimal".

But, when it comes to a complicated one like this, Set description for query parameter in swagger doc using Pydantic model, it is better to use a "custom dependency class"

from fastapi import Depends, FastAPI, Query

app = FastAPI()


class Model:
    def __init__(
            self,
            y: str,
            x: str = Query(
                default='default for X',
                title='Title for X',
                deprecated=True
            )

    ):
        self.x = x
        self.y = y


@app.post("/test-body")
def test_body(model: Model = Depends()):
    return model

If you are using this method, you will have more control over the OpenAPI doc.

JPG
  • 82,442
  • 19
  • 127
  • 206
  • Are you aware you can also use `Query` definitions in pydantic models? – h345k34cr Oct 20 '20 at 09:45
  • @h345k34cr Yes, I do. Unfortunately, it doesn't work for the ***linked SO post*** – JPG Oct 20 '20 at 09:51
  • It is also worth to add a minimal example here as a new answer so that someone else can benefit from that. @h345k34cr – JPG Oct 20 '20 at 09:54
0

@cglacet 's answer is simple and works, but it will raise pydantic ValidationError when validation fail and not gonna pass the error to client. You can find reason here.

This works and pass message to client. Code from here.

import inspect

from fastapi import  Query, FastAPI, Depends
from pydantic import BaseModel, ValidationError
from fastapi.exceptions import  RequestValidationError


class QueryBaseModel(BaseModel):
    def __init_subclass__(cls, *args, **kwargs):
        field_default = Query(...)
        new_params = []
        for field in cls.__fields__.values():
            default = Query(field.default) if not field.required else field_default
            annotation = inspect.Parameter.empty

            new_params.append(
                inspect.Parameter(
                    field.alias,
                    inspect.Parameter.POSITIONAL_ONLY,
                    default=default,
                    annotation=annotation,
                )
            )

        async def _as_query(**data):
            try:
                return cls(**data)
            except ValidationError as e:
                raise RequestValidationError(e.raw_errors)

        sig = inspect.signature(_as_query)
        sig = sig.replace(parameters=new_params)
        _as_query.__signature__ = sig  # type: ignore
        setattr(cls, "as_query", _as_query)

    @staticmethod
    def as_query(parameters: list) -> "QueryBaseModel":
        raise NotImplementedError

class ParamModel(QueryBaseModel):
    start_datetime: datetime
    
app = FastAPI()

@app.get("/api")
def test(q_param: ParamModel: Depends(ParamModel.as_query))
    start_datetime = q_param.start_datetime
    ...
    return {}

rumbarum
  • 803
  • 7
  • 7
0

Using fastapi (0.101.1) and pydantic (v2):

from typing import Annotated
from fastapi import Depends
from pydantic import BaseModel, Field

class Model(BaseModel):
   query_param1: str = Field(...)
   query_param2: int | None = Field(None)


@app.get("")
async def _(query_params: Model = Depends()):
        ...