Fastapi: Serialise FORM bodies with pydantic via type annotiations

Created on 31 Aug 2020  路  8Comments  路  Source: tiangolo/fastapi

Description

I started to use FastAPI and enjoyed serialisation of JSON bodies into pydantic models via type annotations and then I passed the form to my request handler and was surprised with AttributeError.

So I re-read the docs about forms and found its behaviour inconsistent with JSON bodies even though probably all code might already support this.

Example

from fastapi import FastAPI, Request, Form
from pydantic import BaseModel

app = FastAPI()


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


@app.post("/item_form", response_model=Item)
async def find_item(r: Request):
    # current workaround
    form = await r.form()
    item = Item(**form)
    return item


@app.post("/item", response_model=Item)
async def find_item(r: Request, item: Item):
    # JSON Body works nicely
    print(item.name)
    return item


@app.post("/item_form_a", response_model=Item)
async def find_item(item: Item = Form(...)):
    # alternative 1
    # current: 422 Unprocessable Entity
    return item


class ItemForm(BaseModel):
    name: str = Form(...)
    another: str = Form(...)


@app.post("/item_form_b", response_model=ItemForm)
async def find_item(item: ItemForm):
    # alternative 2
    # current: AttributeError and 422 Unprocessable Entity
    print(item.name)
    return item

Motivation

I'm building a slack bot application which receives slash commands from Slack. POST request received from slack is an URL encoded body. https://api.slack.com/web#slack-web-api__basics__post-bodies__url-encoded-bodies

enhancement

Most helpful comment

@1oglop1 Could you close the issue

alt-text

All 8 comments

Please make sure you have installed the python-multipart library to enable form body support

@Mause I did, it did not change anything. I was talking to @ycd about this via gitter.
AFAIK python-multipart is mainly for support of files.

You can do something like this

class AnyForm(BaseModel):
    name: str
    another: str


@app.post("/item_form_b", response_model=AnyForm)
async def find_item(name: str = Form(...), another: str = Form(...)):
    any_form = AnyForm(name=name, another=another)
    return any_form

or you can also use Depends()

class AnyForm(BaseModel):
    name: str
    another: str

    @classmethod
    def as_form(cls, name: str = Form(...), another: str = Form(...)):
        return cls(name=name, another=another)


@app.post("/item_form_a", response_model=AnyForm)
async def any_view(form_data: AnyForm = Depends(AnyForm.as_form)):
    return form_data

Both of those look nice in the /docs as well 馃檪 (something you won't get with await request.Form())

Annotation 2020-09-01 003056

Which you use is up to you, i prefer the first example since i'm not a fan of using Depends for too many stuff.

This is another possibility:

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

@ArcLightSlavik Thank you for providing examples
I'd really like to avoid the first option because it goes agains general advice of having maximum 5 parameters per function.
The second option with Depends looks like something I could use!

@Mause Could you please provide more complete example? I'm not sure I used it right because I got an error
This is how I tried to use it.

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("/item")
async def find_item(item: Item):
    print(item.name)
    return item

Sorry, should have given a full example:

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(Item)):
    return item


tc = TestClient(app)


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

assert r.status_code == 200
assert r.json() == {'name': 'name', 'another': 'another'}

Thanks @Mause this is just perfect for my use-case!

@1oglop1 Could you close the issue

alt-text

Was this page helpful?
0 / 5 - 0 ratings