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 using
application/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.
Method 1
As described here, one can define files and form data at the same time using File
and Form
fields. 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")
async 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 still use a Pydantic model with the above method if you wish, by creating a new instance of the Pydantic model using the received data, as
shown below:
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False @app.post("/submit")
@app.post("/submit")
...
return {"JSON Payload ": Base(name=name, point=point, is_accepted=is_accepted), "Filenames": [file.filename for
file in files]}
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 using OpenAPI 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())
Method 2
One can use Pydantic models, along with Dependencies to inform the “submit” route (in the case below) that the parameterised 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")
async def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
received_data= base.dict()
return {"JSON Payload ": received_data, "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 (which uses Javascript to modify the action
attribute of the form
, in order to pass the form
data as query
params to the URL).
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 also use OpenAPI docs, or Python requests, as shown in the example below. Note: this time params=payload
is used, as the parameters are query
params, not body
(data) params.
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())
Method 3
Another option would be to pass the body data as a single parameter (of type Form
) in the form of a JSON
string. On server side, you can create a dependency function, where you parse the data using parse_raw
method and validate the data against the corresponding model. If ValidationError
is raised, an HTTP_422_UNPROCESSABLE_ENTITY
error is sent back to the client, including the error message. Example is given below:
app.py
from fastapi import FastAPI, status, Form, UploadFile, File, Depends, Request
from pydantic import BaseModel, ValidationError
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
async def checker(data: str = Form(...)):
try:
model = Base.parse_raw(data)
except ValidationError as e:
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return model
@app.post("/submit")
async def submit(model: Base = Depends(checker), files: List[UploadFile] = File(...)):
return {"JSON Payload ": model, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
test.py
Note that in JSON
, boolean values are lower case (i.e., true
and false
), whereas in Python they are capitalised (True
and False
).
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'))]
data = {'data': '{"name": "foo", "point": 0.13, "is_accepted": false}'}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
Or, if you prefer:
import requests
import json
url="http://127.0.0.1:8000/submit"
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': json.dumps({"name": "foo", "point": 0.13, "is_accepted": False})}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
Test using Fetch API or Axios
templates/index.html
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput" name="file" multiple><br>
<input type="button" value="Submit using fetch" onclick="submitUsingFetch()">
<input type="button" value="Submit using axios" onclick="submitUsingAxios()">
<script>
function submitUsingFetch() {
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
for (const file of fileInput.files)
formData.append('files', file);
fetch('/submit', {
method: 'POST',
body: formData,
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
}
function submitUsingAxios() {
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
for (const file of fileInput.files)
formData.append('files', file);
axios({
method: 'POST',
url: '/submit',
data: formData,
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
}
</script>
</body>
</html>
Method 4
A likely preferable method comes from the discussion here, and incorporates a custom class with a classmethod used to transform a given JSON
string into a Python dictionary, which is then used for validation against the Pydantic model. Similar to Method 3 above, the input data should be passed as a single Form
parameter in the form of JSON
string. Thus, the same test.py file(s) and index.html template from the previous method can be used for testing the below.
app.py
from fastapi import FastAPI, File, Form, UploadFile, Request
from pydantic import BaseModel
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import json
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
@classmethod
def __get_validators__(cls):
yield cls.validate_to_json
@classmethod
def validate_to_json(cls, value):
if isinstance(value, str):
return cls(**json.loads(value))
return value
@app.post("/submit")
def submit(data: Base = Form(...), files: List[UploadFile] = File(...)):
return {"JSON Payload ": data, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})