Reputation: 815
This is my Pydantic model:
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
This is the endpoint:
def create_base(
base: Base = Form(...),
file: List[UploadFile] = File(...)
):
...
I'm trying to send a request via multipart form, but i'm getting the error:
{
"detail": [
{
"loc": [
"body",
"base"
],
"msg": "value is not a valid dict",
"type": "type_error.dict"
}
]
}
This is the payload of my request:
{
"name": "string",
"point": 10.0,
"is_accepted": true
}
What am I doing wrong?
Upvotes: 4
Views: 9118
Reputation: 34055
Please have a look at this answer for more details and options.
As per FastAPI documentation,
You can declare multiple
Form
parameters in a path operation, but you can't also declareBody
fields that you expect to receive asJSON
, as the request will have the body encoded usingapplication/x-www-form-urlencoded
instead ofapplication/json
(when the form includes files, it is encoded asmultipart/form-data
).This is not a limitation of FastAPI, it's part of the
HTTP
protocol.
So, as described here, one can define files and form fields at the same time using File
and Form
. Below is a working example:
app.py
from fastapi import Form, File, UploadFile, Request, FastAPI
from typing import List
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.post("/submit")
def submit(name: str = Form(...), point: float = Form(...), is_accepted: bool = Form(...), files: List[UploadFile] = File(...)):
return {"JSON Payload ": {"name": name, "point": point, "is_accepted": is_accepted}, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
You can test it by accessing the template below at http://127.0.0.1:8000
templates/index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" action="http://127.0.0.1:8000/submit" enctype="multipart/form-data">
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
<label for="file">Choose files to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
</body>
</html>
You can also test it through OpenAPI docs (Swagger UI) at http://127.0.0.1:8000/docs or Python requests, as shown below:
test.py
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
payload ={"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, data=payload, files = files)
print(resp.json())
One can use Pydantic models, along with Dependencies to inform the "submit" route (in the case below) that the parameterized variable base
depends on the Base
class. Please note, this method expects the base
data as query (not body) parameters (which are then converted into an equivalent JSON Payload using .dict()
method) and the Files as multipart/form-data
in the body.
app.py
from fastapi import Form, File, UploadFile, Request, FastAPI, Depends
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Optional
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
@app.post("/submit")
def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
received_data= base.dict()
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Again, you can test it with the template below:
templates/index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" id="myForm" onclick="transformFormData();" enctype="multipart/form-data">
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
<label for="file">Choose files to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
<script>
function transformFormData(){
var myForm = document.getElementById('myForm');
var qs = new URLSearchParams(new FormData(myForm)).toString();
myForm.action = 'http://127.0.0.1:8000/submit?'+qs;
}
</script>
</body>
</html>
As mentioned earlier you can use Swagger UI, or the Python requests example below:
test.py
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
payload ={"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, params=payload, files=files)
print(resp.json())
Upvotes: 4