8
votes

I am trying to submit data from html forms and on the validate it with 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

How ever I get the error with http status 422 Unprocessable Entity

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

Equivalent curl command (generated by firfox) 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?

4
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
In the above code GET request gives a HTML form, I click submit on that. I get error for all values i give. - shanmuga
The first step to working out what's going wrong is to inspect the POST request and see what's being submitted. - SColvin
The request body contains no=1&nm=abcd - shanmuga

4 Answers

18
votes

I found a solution which can help us to use FastAPI forms as pydantic as well :)
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 showed in the swagger as usual form
I think, it can be written more generic, maybe I will return and edit the answer.

[UPDATED]

I've written it 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

        if not model_field.required:
            new_parameters.append(
                inspect.Parameter(
                    model_field.alias,
                    inspect.Parameter.POSITIONAL_ONLY,
                    default=Form(model_field.default),
                    annotation=model_field.outer_type_,
                )
            )
        else:
            new_parameters.append(
                inspect.Parameter(
                    model_field.alias,
                    inspect.Parameter.POSITIONAL_ONLY,
                    default=Form(...),
                    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 usage looks like

class Test1(BaseModel):
    a: str
    b: int


@as_form
class Test(BaseModel):
    param: str
    test: List[Test1]
    test1: Test1
    b: int = 1
    a: str = '2342'


@router.post('/me', response_model=Test)
async def me(request: Request, form: Test = Depends(Test.as_form)):
    return form
3
votes

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)
2
votes

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
0
votes

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'}