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.
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
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
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())

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

Most helpful comment
@1oglop1 Could you close the issue