I have the following code example:
from fastapi import File, UploadFile, Request, FastAPI, Depends
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI()
class BaseBox(BaseModel):
l: float=Field(...)
t: float=Field(...)
r: float=Field(...)
b: float=Field(...)
class BaseInput(BaseModel):
boxes: List[BaseBox] = Field(...)
words: List[str] = Field(...)
width: Optional[float] = Field(...)
height: Optional[float] = Field(...)
@app.post("/submit")
def submit(
base_input: BaseInput = Depends(),
file: UploadFile = File(...), # Add this line to accept a file
):
return {
"JSON Payload": base_input,
"Filename": file.filename,
}
@app.get("/")
def main(request: Request):
return {"status":"alive"}
but some how I can not make it works. I use the interactive API docs, but I always get an error. Do you think I have to send 2 files instead? I did also try with
curl -X 'POST' \
'http://localhost:8007/submit?width=10&height=10' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@test.png;type=image/png' \
-F 'boxes={
"l": 0,
"t": 0,
"r": 0,
"b": 0
}' \
-F 'words=test,test2,tes3,test'
but I always get the error
"POST /submit?width=10&height=10 HTTP/1.1" 422 Unprocessable Entity
.
As can be seen in the code you provided, you already had a look at this answer, and that answer should help you find the solution you are looking for in the end. Related answers can also be found here and here.
But, first, please let me explain what is wrong with the code in your example. You are submitting both Files and query data, or, at least, that is what seems you have been trying to achieve. Having a query parameter defined, for instance, as str
or int
, in an endpoint—or having a Pydantic BaseModel
along with using Depends()
on a parameter in the endpoint to indicate that the fileds defined in the model are expected as query parameters—in either case, that should work just fine. However, when you define a query parameter as a List
, e.g., List[int]
or List[str]
, either directly in the endpoint or in a BaseModel
, you should define it explicitly with Query
, as explained and demonstrated here and here. While Pydantic models did not allow the use of Query
fields in the past, and one had to implement the query parameter-parsing in a separate dependency class, as shown in this answer and this answer, this has recently been changed, and hence, one could instead wrap the Query()
in a Field()
using a BaseModel
class, as demonstrated in this answer.
from fastapi import FastAPI, Query, Depends
from pydantic import BaseModel, Field
from typing import Optional, List
app = FastAPI()
class Base(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
@app.get('/')
async def main(base: Base = Depends()):
return base
In your example, you also seem to have a List
query parameter that expects as a value a list of dictionary/JSON objects. That, however, cannot be achieved using query parameters. If you attempted defining such a parameter (e.g., boxes: List[BaseBox] = Field (Query(...))
) in the working example above, you would come across the following error when trying to run the FastAPI application:
AssertionError: Param:
boxes
can only be a request body, usingBody()
If you instead defined the parameter as boxes: List[BaseBox] = Field (...)
, along with file: UploadFile = File(...)
defined in the endpoint, as you have already been doing in your code, even though the application would start running as usual, when attempting to submit a request to that endpoint (through Swagger UI autodocs at /docs
, for instance), you would receive a 422 Unprocessable Entity
error, with a message that has a similar meaning, saying that Input should be a valid dictionary or object to extract fields from
.
That is because, in the first case, you can't have a query parameter expecting dictionary data (unless you followed the approach described here and here for arbitrary query data, which you would need to parse on your own, and which I would not recommend doing), while, in the second case, the request body is sent encoded as multipart/form-data
due to the file: UploadFile = File(...)
field defined in the endpoint; however, sending both Form and JSON data together is not supported by the HTTP protocol (again, see this answer).
If, however, you removed the UploadFile
parameter from the endpoint, the request should go through successfully, as the request body would then be endoced as application/json
(have a look at the Content-Type
request header in Swagger UI when submitting the request). In that case, however, you should use a POST
request—and hence, define your endpoint with @app.post('/')
, for instance—not a GET
one, as a request with GET
/HEAD
method shouldn't have a body (Requests using GET
method should only be used to request data; they shouldn't include data).
from fastapi import FastAPI, Query, Depends
from pydantic import BaseModel, Field
from typing import Optional, List
app = FastAPI()
class BaseBox(BaseModel):
l: float = Field(...)
t: float = Field(...)
class Base(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
boxes: List[BaseBox] = Field (...)
@app.post('/')
async def main(base: Base = Depends()):
return base
List
of dictionaries)If you still had to add both File(s) and JSON body in a FastAPI POST request, I would highly suggest having a look at Methods 3 and 4 of this answer. The examples provided below are based on those two approaches in the linked answer, and demonstrate how to post Files together with JSON data that also include lists of dictionaries, just like in your example. Please have a look at the linked answer for further details and examples in Python and JavaScript on how to test the approaches. In your case, you would need to separate query parameters from body fields, and have query parameters defined either in the endpoint (as explained in the linked answers provided earlier), or in separate Pydantic models, as demonstrated below.
In Swagger UI /docs
—since data
is a Form
parameter and represented as a single field—you would need to pass the data for the Base
model in that field as a dictionary, which would be submitted as str
in the data
Form
parameter. Client-side test example (for the server-side example given below):
{"boxes": [{"l": 0,"t": 0,"r": 0,"b": 0}], "comments": ["foo", "bar"], "code": 0}
For further information on how to test this, see the linked answer above.
app.py
from fastapi import FastAPI, status, Form, UploadFile, File, Depends, Query
from pydantic import BaseModel, Field, ValidationError
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from typing import Optional, List
app = FastAPI()
class BaseParams(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
class BaseBox(BaseModel):
l: float=Field(...)
t: float=Field(...)
r: float=Field(...)
b: float=Field(...)
class Base(BaseModel):
boxes: List[BaseBox] = Field (...)
comments: List[str] = Field (...)
code: int = Field (...)
def checker(data: str = Form(...)):
try:
return Base.model_validate_json(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@app.post("/submit")
def submit(
base_params: BaseParams = Depends(),
base: Base = Depends(checker),
files: List[UploadFile] = File(...),
):
return {
"Params": base_params,
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}
This approach has the advantage of requiring less code to achieve the expected outcome, as well as the Base
model is represented (with an automatically generated input example) in the request body section of Swagger UI /docs
, resulting in a more clear view of the data and easier way of posting the data. Again, please have a look at the linked answer above for more details on this approach.
app.py
from fastapi import FastAPI, Body, UploadFile, File, Depends, Query
from pydantic import BaseModel, Field, model_validator
from typing import Optional, List
import json
app = FastAPI()
class BaseParams(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
class BaseBox(BaseModel):
l: float=Field(...)
t: float=Field(...)
r: float=Field(...)
b: float=Field(...)
class Base(BaseModel):
boxes: List[BaseBox] = Field (...)
comments: List[str] = Field (...)
code: int = Field (...)
@model_validator(mode="before")
@classmethod
def validate_to_json(cls, value):
if isinstance(value, str):
return cls(**json.loads(value))
return value
@app.post("/submit")
def submit(
base_params: BaseParams = Depends(),
base: Base = Body(...),
files: List[UploadFile] = File(...),
):
return {
"Params": base_params,
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}