Fastapi: Expand model binding beyond posted body (universal Pydantic inbound data)

Created on 30 Oct 2020  路  4Comments  路  Source: tiangolo/fastapi

First check

  • [ x] I added a very descriptive title to this issue.
  • [ x] I used the GitHub search to find a similar issue and didn't find it.
  • [ x] I searched the FastAPI documentation, with the integrated search.
  • [ x] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [ x] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [ x] I already checked if it is not related to FastAPI but to Pydantic.
  • [ x] I already checked if it is not related to FastAPI but to Swagger UI.
  • [ x] I already checked if it is not related to FastAPI but to ReDoc.
  • [ x] After submitting this, I commit to:

    • Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.

    • Or, I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.

    • Implement a Pull Request for a confirmed bug.

Example

I'm sorry if this is a duplicate or I've missed how to do this. I have scoured the docs and github issues.

Here's a self-contained minimal, reproducible, example with my use case:

# URL request: localhost/api/weather/portland?state=OR&country=US&units=imperial

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
import uvicorn

class Location(BaseModel):
    city: str
    country: Optional[str] = 'US'
    state: Optional[str] = None
    units: Optional[str] = 'metric'

app = FastAPI()

# What we have is:
@app.get('/api/weather/{city}')
async def weather(city: str,
        country: Optional[str] = None,
        state: Optional[str] = None,
        units: str = 'metric'):
    # work with city, country, etc.
    return Location(city=city, country=country, state=state, units=units)

# What I want:
# @app.get('/api/weather_binding/{city}')
# async def weather(loc: Location):
    # work with loc.city, loc.country, etc.
    # Where the binding inputs to the Pydantic model are basically:
    # data = {
    #       **request.query_params,
    #       **request.headers,
    #        **dict(await request.form()),  # only if the form dependencies are installed
    #        **request.path_params
    #    }
    # return loc

uvicorn.run(app)

Description

  • Open the browser and call the endpoint /api/weather/portland?state=OR&country=US&units=imperial.
  • It returns a JSON:
{
  "city": "portland",
  "country": null,
  "state": "OR",
  "units": "metric"
}
  • I would like to use model binding just like we have for JSON POST of inbound Pydantic models but combining all these elements.

The solution you would like

Basically, I can do this already with this step:

data = {
    **request.query_params,
    **request.headers,
    **dict(await request.form()),
    **request.path_params
}
loc = Location(**data)

But that's a drag and having the framework do this, even with an opt in flag in the route (like bind_all=True) would be fantastic.

Describe alternatives you've considered

I've consider just using the lines of code above more or less on method, but that doesn't seem to fit with the clean automatic nature of the platform.

Environment

  • OS: macOS
  • FastAPI Version: 0.61.1
  • Python version: 3.9

Additional context

None

enhancement

Most helpful comment

It's also possible to do something like this:

from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends, Form
from pydantic import BaseModel


app = FastAPI()


def form_body(cls):
    cls.__signature__ = cls.__signature__.replace(
        parameters=[
            arg.replace(default=Form(...))
            for arg in cls.__signature__.parameters.values()
        ]
    )
    return cls


@form_body
class Item(BaseModel):
    name: str
    another: str


@app.post('/test', response_model=Item)
def endpoint(item: Item = Depends()):
    return item


tc = TestClient(app)


r = tc.post('/test', data={'name': 'name', 'another': 'another'})

assert r.status_code == 200, r
print(r.json())

All 4 comments

I think you are looking for Depends here.

from fastapi import Depends

@app.get("/api/weather_binding/{city}")
async def weather(loc: Location = Depends()):
    return loc

Open your browser go to http://127.0.0.1:8000/api/weather_binding/New%20Mexico?country=US&state=oregon&units=metric

{
  "city": "Nashville",
  "country": "US",
  "state": "Tennessee",
  "units": "metric"
}

It really understands what you are trying to do. Normally GET requests can not contain a request body as OpenAPI specification points it out clearly in here. Without Depends, it assumes it is a schema and expects a request body. But with Depends it understands it is a GET operation it turns that into parameters. But if you use Depends in a POST operation you'll have a request body whether you have a Depends or not.

Thanks @ycd This is close but it doesn't consider header values or body values. However taking query params and route params is pretty close to what I had in mind. Definitely appreciate the pointer.

That said, I would never ever think dependency injection == reading values from query string. To me, dependency injection is all about I register something like a DB concrete type at app startup, the method says it takes one, I get the instance or type I registered. But that is entirely outside the bounds of the request, just the app config. I think of dependency injection much like pytest fixtures.

So, it's super confusing to me that these things coexist in the same feature / help location. :) Thank you for helping me on it.

It's also possible to do something like this:

from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends, Form
from pydantic import BaseModel


app = FastAPI()


def form_body(cls):
    cls.__signature__ = cls.__signature__.replace(
        parameters=[
            arg.replace(default=Form(...))
            for arg in cls.__signature__.parameters.values()
        ]
    )
    return cls


@form_body
class Item(BaseModel):
    name: str
    another: str


@app.post('/test', response_model=Item)
def endpoint(item: Item = Depends()):
    return item


tc = TestClient(app)


r = tc.post('/test', data={'name': 'name', 'another': 'another'})

assert r.status_code == 200, r
print(r.json())

Great, thank you @Mause Depends() wasn't obvious but it does pretty much achieve what I was hoping for. :)

Was this page helpful?
0 / 5 - 0 ratings