When I send a multi-part form data it returns the error: "Unprocessable Entity"
the api endpoint:
#router.post('/', response_model=schemas.Category)
def add_category(
file: UploadFile,
category: schemas.CategoryCreate = Depends(schemas.CategoryCreate.as_form),
db: Session = Depends(get_db)
):
print(category)
print(file.filename)
The BaseModel classes:
class ProductCreate(BaseModel):
title: str
description: str
price: float
time: float
photo_url: str
#as_form
class CategoryCreate(BaseModel):
name: str
products: List[ProductCreate] = []
the as_form function:
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
note :
When I remove the field:
products: List[ProductCreate] = []
from CategoryCreate BaseModel, it works fine!
Thank you in advance
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?
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 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
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)
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'}
You can do this even simpler using dataclasses
from dataclasses import dataclass
from fastapi import FastAPI, Form, Depends
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>'''
#dataclass
class SimpleModel:
no: int = Form(...)
nm: str = Form(...)
#app.post("/form")
def form_post(form_data: SimpleModel = Depends()):
return form_data
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
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 from 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)
Tldr: a mypy compliant, inheritable version of other solutions that produces the correct generated OpenAPI schema field types rather than any/unknown types.
Existing solutions set the FastAPI params to typing.Any to prevent the validation from occurring twice and failing, this causes the generated API spec to have any/unknown param types for these form fields.
This solution temporarily injects the correct annotations to the routes before schema generation, and resets them in line with other solutions afterwards.
# Example usage
class ExampleForm(FormBaseModel):
name: str
age: int
#api.post("/test")
async def endpoint(form: ExampleForm = Depends(ExampleForm.as_form)):
return form.dict()
form_utils.py
import inspect
from pydantic import BaseModel, ValidationError
from fastapi import Form
from fastapi.exceptions import RequestValidationError
class FormBaseModel(BaseModel):
def __init_subclass__(cls, *args, **kwargs):
field_default = Form(...)
new_params = []
schema_params = []
for field in cls.__fields__.values():
new_params.append(
inspect.Parameter(
field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(field.default) if not field.required else field_default,
annotation=inspect.Parameter.empty,
)
)
schema_params.append(
inspect.Parameter(
field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(field.default) if not field.required else field_default,
annotation=field.annotation,
)
)
async def _as_form(**data):
try:
return cls(**data)
except ValidationError as e:
raise RequestValidationError(e.raw_errors)
async def _schema_mocked_call(**data):
"""
A fake version which is given the actual annotations, rather than typing.Any,
this version is used to generate the API schema, then the routes revert back to the original afterwards.
"""
pass
_as_form.__signature__ = inspect.signature(_as_form).replace(parameters=new_params) # type: ignore
setattr(cls, "as_form", _as_form)
_schema_mocked_call.__signature__ = inspect.signature(_schema_mocked_call).replace(parameters=schema_params) # type: ignore
# Set the schema patch func as an attr on the _as_form func so it can be accessed later from the route itself:
setattr(_as_form, "_schema_mocked_call", _schema_mocked_call)
#staticmethod
def as_form(parameters=[]) -> "FormBaseModel":
raise NotImplementedError
# asgi.py
from fastapi.routing import APIRoute
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.dependencies.utils import get_dependant, get_body_field
api = FastAPI()
def custom_openapi():
if api.openapi_schema:
return api.openapi_schema
def create_reset_callback(route, deps, body_field):
def reset_callback():
route.dependant.dependencies = deps
route.body_field = body_field
return reset_callback
# The functions to call after schema generation to reset the routes to their original state:
reset_callbacks = []
for route in api.routes:
if isinstance(route, APIRoute):
orig_dependencies = list(route.dependant.dependencies)
orig_body_field = route.body_field
is_modified = False
for dep_index, dependency in enumerate(route.dependant.dependencies):
# If it's a form dependency, set the annotations to their true values:
if dependency.call.__name__ == "_as_form": # type: ignore
is_modified = True
route.dependant.dependencies[dep_index] = get_dependant(
path=dependency.path if dependency.path else route.path,
# This mocked func was set as an attribute on the original, correct function,
# replace it here temporarily:
call=dependency.call._schema_mocked_call, # type: ignore
name=dependency.name,
security_scopes=dependency.security_scopes,
use_cache=False, # Overriding, so don't want cached actual version.
)
if is_modified:
route.body_field = get_body_field(dependant=route.dependant, name=route.unique_id)
reset_callbacks.append(
create_reset_callback(route, orig_dependencies, orig_body_field)
)
openapi_schema = get_openapi(
title="foo",
version="bar",
routes=api.routes,
)
for callback in reset_callbacks:
callback()
api.openapi_schema = openapi_schema
return api.openapi_schema
api.openapi = custom_openapi # type: ignore[assignment]
I would like to make that my query returns something like that:
{"message": "OK",
"data": {
"username": "string",
"pseudo": "string",
"email": "string"}
}
But i cant make my model return inside JSON i am only capable of returning ONLY the model so it gives that :
{
"username": "string",
"pseudo": "string",
"email": "string"
}
That is the code that i tried to run to get the first code snippet
#app.post("/", response_model=_models.UserOut, status_code=status.HTTP_201_CREATED)
def postUser(userPost: _models.UserIn):
if not userPost.regex_check_email(userPost.email):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Email: {userPost.email} not valid format")
if not userPost.regex_check_username(userPost.username):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Username: {userPost.username} not valid format")
else:
db_user.append(userPost)
return {"message": "OK",
"data": userPost}
and my models :
class UserOut(BaseModel):
username: str
pseudo: Optional[str] = None
email: str
class UserIn(UserOut):
username: str
pseudo: Optional[str] = None
email: str
password: str
def regex_check_email(self, email):
match = re.match(email_regex, email)
is_match_email = bool(match)
return is_match_email
def regex_check_username(self, username):
match = re.match(username_regex, username)
is_match_username = bool(match)
return is_match_username
this only returns me
pydantic.error_wrappers.ValidationError: 2 validation errors for
UserOut response -> username field required
(type=value_error.missing) response -> email field required
(type=value_error.missing)
It would be great if u could help me and tell me why i was failing, i think i did not understood everything about response models. Thanks.
In the header of your POST handler you are using
#app.post("/", response_model=_models.UserOut ...)
...
So when you try to return something that doesn't match the UserOut model it returns an error.
Create a model that defines your message and data structure as desired and use that as the response_model
Happy coding
Edit: Have you tried using something like this: (python 3.10+)
class UserData(BaseModel):
username: str
pseudo: Optional[str] = None
email: str
class UserOut(BaseModel):
message: str
data: UserData | None = None
When in doubt, the official documentation usually has examples.
In the code, if it is done using (#app.post('/...')), it runs without any problems, but if it is done using (#router.post('/...')), it gives the following error;
TypeError: Cannot instantiate typing.Union
code:
router = APIRouter()
#app.get("/api/accounts/userslist/{domain}",
response_description="List all users")
async def account_list(
domain: str,
credentials: HTTPAuthorizationCredentials = Security(security)):
token = credentials.credentials
if (auth_handler.decode_token(token)):
domain = domains_db.find_one({'domain': domain})
if domain == None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content='There is no user matching the requested domain.')
else:
accounts = users_db.find({'domain': domain['domain_id']})
list_account = [AuthModel(**account) for account in accounts]
return list_account
model:
class AuthModel(BaseModel):
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
userid: UUID = Field(default_factory=uuid4)
firstname: Optional[str]
lastname: Optional[str]
domain: UUID = Field(default_factory=uuid4)
email: EmailStr
phone: Optional[str]
plain_secret: Optional[str]
status: Optional[str]
Are you including the router in the app in your main.py file as described here: https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-the-apirouters-for-users-and-items ?
how to custom field.url in flask-restful.
user_fields = {
...
'test': fields.Url('userep', absolute=True)
....
}
api.add_resource(User, '/user', '/user/<int:userid>', endpoint='userep')
when i submit http://127.0.0.1:5000/user/1
the result is like this : "test": "http://127.0.0.1:5000/user",
and change user_fields like this:
user_fields = {
'id': fields.Integer,
'friends': fields.Url('/Users/{id}/Friends'),
when i submit http://127.0.0.1:5000/user/1
throw error like those:
werkzeug.routing.BuildError
BuildError: Could not build url for endpoint '/Users/{id}/Friends'
with values ['_sa_instance_state', 'email', 'id', 'nickname',
'password', 'regist_date', 'status']. Did you mean 'version' instead?
any advise? thx
for further,if i change resource
api.add_resource(User, '/user/<int:userid>', endpoint='userep')
the error message throw
werkzeug.routing.BuildError
BuildError: Could not build url for endpoint 'userep' with values
['_sa_instance_state', 'email', 'id', 'nickname', 'password',
'regist_date', 'status']. Did you forget to specify values ['userid']?
in official document field.url
class Url(Raw):
"""
A string representation of a Url
:param endpoint: Endpoint name. If endpoint is ``None``,
``request.endpoint`` is used instead
:type endpoint: str
:param absolute: If ``True``, ensures that the generated urls will have the
hostname included
:type absolute: bool
:param scheme: URL scheme specifier (e.g. ``http``, ``https``)
:type scheme: str
"""
def __init__(self, endpoint=None, absolute=False, scheme=None):
super(Url, self).__init__()
self.endpoint = endpoint
self.absolute = absolute
self.scheme = scheme
def output(self, key, obj):
try:
data = to_marshallable_type(obj)
endpoint = self.endpoint if self.endpoint is not None else request.endpoint
o = urlparse(url_for(endpoint, _external=self.absolute, **data))
if self.absolute:
scheme = self.scheme if self.scheme is not None else o.scheme
return urlunparse((scheme, o.netloc, o.path, "", "", ""))
return urlunparse(("", "", o.path, "", "", ""))
except TypeError as te:
raise MarshallingException(te)
flask_restful/fields.py
answer by myself: this is no way to resolve problem.
according flask-restful project issue: api.url_for() fails with endpoints specified by a string and Flask jsonify a list of objects
code like this:
from flask import url_for
class ProjectsView(object):
def __init__(self, projectid):
self.projectid = projectid
...
def serialize(self):
return {
...
'tasks_url':url_for('.getListByProjectID', _external=True, projectid=self.projectid),
}
class Projects(Resource):
def get(self, userid):
project_obj_list = []
...
v = ProjectsView(project.id)
project_obj_list.append(v)
return jsonify(result=[e.serialize() for e in project_obj_list])
and the response like this:
{
"result": [
{
...
"tasks_url":"http://127.0.0.1:5000/api/v1.0/1/GetListByProjectID"
}
]
}