26

I am trying to submit data from HTML forms and validate it with a Pydantic model.

Using this code

from fastapi import FastAPI, Form
from pydantic import BaseModel
from starlette.responses import HTMLResponse


app = FastAPI()

@app.get("/form", response_class=HTMLResponse)
def form_get():
    return '''<form method="post"> 
    <input type="text" name="no" value="1"/> 
    <input type="text" name="nm" value="abcd"/> 
    <input type="submit"/> 
    </form>'''


class SimpleModel(BaseModel):
    no: int
    nm: str = ""

@app.post("/form", response_model=SimpleModel)
def form_post(form_data: SimpleModel = Form(...)):
    return form_data

However, I get the HTTP error: "422 Unprocessable Entity"

{
    "detail": [
        {
            "loc": [
                "body",
                "form_data"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}

The equivalent curl command (generated by Firefox) is

curl 'http://localhost:8001/form' -H 'Content-Type: application/x-www-form-urlencoded' --data 'no=1&nm=abcd'

Here the request body contains no=1&nm=abcd.

What am I doing wrong?

Gino Mempin
  • 19,150
  • 23
  • 79
  • 104
shanmuga
  • 3,991
  • 2
  • 18
  • 35
  • Well looks like the body is empty, or at least `form_data` is missing. But impossible to help more without seeing what you're submitting. – SColvin Feb 08 '20 at 14:45
  • In the above code GET request gives a HTML form, I click submit on that. I get error for all values i give. – shanmuga Feb 08 '20 at 18:16
  • The first step to working out what's going wrong is to inspect the POST request and see what's being submitted. – SColvin Feb 10 '20 at 12:16
  • The request body contains `no=1&nm=abcd` – shanmuga Feb 11 '20 at 10:36
  • Please have a look at [this](https://stackoverflow.com/a/71439821/17865804) and [this](https://stackoverflow.com/a/70640522/17865804) answer as well. – Chris Apr 30 '22 at 12:58

5 Answers5

36

I found a solution that can help us to use Pydantic with FastAPI forms :)

My code:

class AnyForm(BaseModel):
    any_param: str
    any_other_param: int = 1

    @classmethod
    def as_form(
        cls,
        any_param: str = Form(...),
        any_other_param: int = Form(1)
    ) -> AnyForm:
        return cls(any_param=any_param, any_other_param=any_other_param)

@router.post('')
async def any_view(form_data: AnyForm = Depends(AnyForm.as_form)):
        ...

It's shown in the Swagger as a usual form.

It can be more generic as a decorator:

import inspect
from typing import Type

from fastapi import Form
from pydantic import BaseModel
from pydantic.fields import ModelField

def as_form(cls: Type[BaseModel]):
    new_parameters = []

    for field_name, model_field in cls.__fields__.items():
        model_field: ModelField  # type: ignore

        new_parameters.append(
             inspect.Parameter(
                 model_field.alias,
                 inspect.Parameter.POSITIONAL_ONLY,
                 default=Form(...) if not model_field.required else Form(model_field.default),
                 annotation=model_field.outer_type_,
             )
         )

    async def as_form_func(**data):
        return cls(**data)

    sig = inspect.signature(as_form_func)
    sig = sig.replace(parameters=new_parameters)
    as_form_func.__signature__ = sig  # type: ignore
    setattr(cls, 'as_form', as_form_func)
    return cls

And the usage looks like

@as_form
class Test(BaseModel):
    param: str
    a: int = 1
    b: str = '2342'
    c: bool = False
    d: Optional[float] = None


@router.post('/me', response_model=Test)
async def me(request: Request, form: Test = Depends(Test.as_form)):
    return form
Nikita Davydov
  • 569
  • 3
  • 18
4

you can use data-form like below:

@app.post("/form", response_model=SimpleModel)
def form_post(no: int = Form(...),nm: str = Form(...)):
    return SimpleModel(no=no,nm=nm)
Includeamin
  • 55
  • 1
  • 3
  • Thanks for the answer but this doesn't help. I am asking for specific usage. I am trying to avoid any extra code adding complexity. Also I plan to mix this with other simple variables/files submitted from form. Similar to what can be done using `Path` or `Body` – shanmuga Feb 12 '20 at 10:32
3

If you're only looking at abstracting the form data into a class you can do it with a plain class

from fastapi import Form, Depends

class AnyForm:
    def __init__(self, any_param: str = Form(...), any_other_param: int = Form(1)):
        self.any_param = any_param
        self.any_other_param = any_other_param

    def __str__(self):
        return "AnyForm " + str(self.__dict__)

@app.post('/me')
async def me(form: AnyForm = Depends()):
    print(form)
    return form

And it can also be turned into a Pydantic Model

from uuid import UUID, uuid4
from fastapi import Form, Depends
from pydantic import BaseModel

class AnyForm(BaseModel):
    id: UUID
    any_param: str
    any_other_param: int

    def __init__(self, any_param: str = Form(...), any_other_param: int = Form(1)):
        id = uuid4()
        super().__init__(id, any_param, any_other_param)

@app.post('/me')
async def me(form: AnyForm = Depends()):
    print(form)
    return form
Kassym Dorsel
  • 4,667
  • 1
  • 24
  • 51
2

I implemented the solution found here Mause solution and it seemed to work

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'}
Tonatio
  • 3,436
  • 34
  • 21
stefanlsd
  • 21
  • 1
0

Create the class this way:

from fastapi import Form

class SomeForm:

    def __init__(
        self,
        username: str = Form(...),
        password: str = Form(...),
        authentication_code: str = Form(...)
    ):
        self.username = username
        self.password = password
        self.authentication_code = authentication_code


@app.post("/login", tags=['Auth & Users'])
async def auth(
        user: SomeForm = Depends()
):
    # return something / set cookie
   

Result

If you want then to make an http request form javascript you must use FormData to construct the request:

  const fd = new FormData()
  fd.append('username', username)
  fd.append('password', password)

  axios.post(`/login`, fd)
Ars Hen
  • 26
  • 4
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 26 '22 at 00:10