As per FastAPI documentation:
You can declare multiple Form parameters in a path operation, but you
can't also declare Body fields that you expect to receive as JSON, as
the request will have the body encoded using
application/x-www-form-urlencoded instead of application/json (when the form includes files, it is encoded as multipart/form-data).
This is not a limitation of FastAPI, it's part of the HTTP protocol.
Note that you need to have python-multipart installed, as uploaded files are sent as "form data". For instance:
pip install python-multipart
It should also be noted that in the examples below, the endpoints are defined with normal def, but you could also use async def (depending on your needs). Please have a look at this answer for more details on def vs async def in FastAPI.
If you are looking for how to upload both files and a list of dictionaries/JSON data, please have a look at this answer, as well as this answer and this answer for working examples.
I would also recommend taking a look at this answer, which provides a much faster approach to uploading files (optionally, together with Form or JSON data), as using FastAPI/Starlette's UploadFile (as in the examples provided below) might be quite slow; especially, when uploading rather large files.
Method 1
As described here, one can define files and form fileds at the same time using File and Form. In case you had a large number of parameters and would like to define them separately from the endpoint, please have a look at this answer on how to declare multiple Form fields, using either a dependency class or Pydantic model. Also, for more details on Jinja2Templates, please have a look at the relevant documentation, as well as the first section of this answer, regarding a small change on TemplateResponse recently by Starlette.
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)
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
You could test the above example by accessing the template below at http://127.0.0.1:8000. If your template does not include any Jinja code, you could instead return a simple HTMLResponse. See this answer as well, if you are looking for a JavaScript Fetch API solution.
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="files">Choose file(s) to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
</body>
</html>
You could also test this example using the interactive OpenAPI/Swagger UI autodocs at /docs, e.g., http://127.0.0.1:8000/docs, or using 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'))]
data = {"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
Method 2
One could also use Pydantic models, along with Dependencies, to inform the /submit endpoint (in the example below) that the parameterized variable base depends on the Base class. Please note that this method expects the base data as query (not body) parameters, which are then validated against and converted into the Pydantic model (in this case, the Base model). Also, note that one should never pass sensitive data through the query string, as this poses serious security risks—please have a look at this answer for more details.
When returning a Pydantic model instance (e.g, base) from a FastAPI endpoint (e.g., /submit), it would automatically be converted into a JSON string, behind the scenes, using the jsonable_encoder, as explained in detail in this answer. However, if you would like to have the model converted into a JSON string on your own within the endpoint, you could use Pydantic's model_dump_json() (in Pydantic V2), e.g., base.model_dump_json(), and return a custom Response directly, as explained in the linked answer earlier; thus, avoiding the use of jsonable_encoder. Otherwise, in order to convert the model into a dict on your own, you could use Pydantic's model_dump() (in Pydantic V2), e.g., base.model_dump(), or simply dict(base) (Note that returning a dict object from an endpoint, FastAPI would still use the jsonable_encoder, behind the scenes, as explained in the linked answer above). You may also have a look at this answer for the relevant Pydantic methods and documentation.
Apart from using a Pydantic model for the query parameters, one could also define query parameters directly in the endpoint, as demonstrated in this answer, as well as this answer and this answer.
Besides the base query parameters, the following /submit endpoint also expects Files encoded as multipart/form-data in the request body.
app.py
from fastapi import Form, File, UploadFile, Request, FastAPI, Depends
from typing import List, Optional
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
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(...)):
return {
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}
@app.get("/", response_class=HTMLResponse)
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
The template below now uses JavaScript to modify the action attribute of the form element, in order to pass the form data as query params to the URL instead of form-data in the request body.
templates/index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" id="myForm" onsubmit="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="files">Choose file(s) 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>
If, instead, you would like to perform a JavaScript fetch() request, you could use the following template (see related answer on submiting HTML form as well):
<!DOCTYPE html>
<html>
<body>
<form id="myForm" >
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>
</form>
<label for="fileInput">Choose file(s) to upload</label>
<input type="file" id="fileInput" onchange="reset()" multiple><br>
<input type="button" value="Submit" onclick="submitUsingFetch()">
<p id="resp"></p>
<script>
function reset() {
var resp = document.getElementById("resp");
resp.innerHTML = "";
resp.style.color = "black";
}
function submitUsingFetch() {
var resp = document.getElementById("resp");
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
for (const file of fileInput.files)
formData.append('files', file);
var myForm = document.getElementById('myForm');
var qs = new URLSearchParams(new FormData(myForm)).toString();
fetch('/submit?' + qs, {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
resp.innerHTML = JSON.stringify(data); // data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
</script>
</body>
</html>
As mentioned earlier, you could also use Swagger UI or Python requests for testing. Note that the data should now be passed to the params(not the data) argument of requests.post(), as the data are now sent as query parameters, not form-data, which was the case in Method 1 earlier.
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'))]
params = {"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, params=params, files=files)
print(resp.json())
Method 3
Another option would be to pass the body data as a JSON string in a single Form parameter. To that end, you would need to create a dependency function on server side.
A dependency is "just a function that can take all the same parameters that a path operation function (also known as endpoint) can take. You can think of it as a path operation function without the decorator". Hence, you would need to declare the dependency in the same way you would do with your endpoint (i.e., the parameters' names and types in the dependency should be the ones expected by FastAPI, when a client sends an HTTP request to that endpoint, e.g., data: str = Form(...)). Then, create a new parameter (e.g., base) in your endpoint, using Depends() and pass the dependency function as a parameter to it (Note: Don't call it directly, meaning don't add the parentheses at the end of your function's name, but instead use Depends(checker), where checker is the name of your dependency function). Whenever a new request arrives, FastAPI will take care of calling your dependency, getting the result and assigning that result to the parameter (e.g., base) in your endpoint. For more details on dependencies, please have a look at the links provided earlier.
In this case, the dependency should be used to parse the (JSON string) data using the parse_raw method (Note: In Pydantic V2 parse_raw has been deprecated and replaced by model_validate_json), as well as validate the data against the corresponding Pydantic model. If ValidationError is raised, an HTTP_422_UNPROCESSABLE_ENTITY error should be sent back to the client, including the error message; otherwise, an instance of that model (e.g., Base) is assigned to the parameter in the endpoint.
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
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: Base = Depends(checker), files: List[UploadFile] = File(...)):
return {"JSON Payload": base, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Generic Checker dependency class
In case you had multiple models and would like to avoid creating a checker function for each model, you could instead create a generic Checker dependency class, as described in the docs (see this answer as well), and use it for every different model in your API. Example:
# ... rest of the code is the same as above
class Other(BaseModel):
msg: str
details: Base
class Checker:
def __init__(self, model: BaseModel):
self.model = model
def __call__(self, data: str = Form(...)):
try:
return self.model.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: Base = Depends(Checker(Base)), files: List[UploadFile] = File(...)):
pass
@app.post("/submit_other")
def submit_other(other: Other = Depends(Checker(Other)), files: List[UploadFile] = File(...)):
pass
Arbitrary JSON data
In case validating the input data against a Pydantic model wasn't important to you, but, instead, you would like to receive arbitrary JSON data and simply check whether or not a valid JSON string was sent by the client, use the below (orjson, as shown here, could be used instead of the standard json module):
# ...
from json import JSONDecodeError
import json
def checker(data: str = Form(...)):
try:
return json.loads(data)
except JSONDecodeError:
raise HTTPException(status_code=400, detail='Invalid JSON data')
@app.post("/submit")
def submit(payload: dict = Depends(checker), files: List[UploadFile] = File(...)):
pass
Alternatively, you could simply use the Json type from Pydantic:
from pydantic import Json
@app.post("/submit")
def submit(data: Json = Form(), files: List[UploadFile] = File(...)):
pass
Test using Python requests
test.py
Note that in JSON, boolean values are represented using the true or false literals in lower case, whereas in Python they must be capitalized as either True or 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())
P.S. To test the /submit_other endpoint (described in the generic Checker class earlier) using Python requests, replace the data attribute in the example above with the one below:
import requests
import json
url = 'http://127.0.0.1:8000/submit_other'
data = {'data': json.dumps({"msg": "Hi", "details": {"name": "bar", "point": 0.11, "is_accepted": True}})}
# ... rest of the code is the same as above
Test using Fetch API or Axios
You might find this answer helpful as well, if you are looking for how to convert entries from HTML <form> into a JSON string.
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" onchange="reset()" multiple><br>
<input type="button" value="Submit using fetch" onclick="submitUsingFetch()">
<input type="button" value="Submit using axios" onclick="submitUsingAxios()">
<p id="resp"></p>
<script>
function reset() {
var resp = document.getElementById("resp");
resp.innerHTML = "";
resp.style.color = "black";
}
function submitUsingFetch() {
var resp = document.getElementById("resp");
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 => response.json())
.then(data => {
resp.innerHTML = JSON.stringify(data); // data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
function submitUsingAxios() {
var resp = document.getElementById("resp");
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 => {
resp.innerHTML = JSON.stringify(response.data); // response.data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
</script>
</body>
</html>
Method 4
A further method comes from the github 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 (Note that, compared to the example given in the aforementioned github link, the example below uses @model_validator(mode='before'), since the introduction of Pydantic V2).
Similar to Method 3 above, the input data should be passed as a single Form parameter in the form of JSON string (Note that defining the data parameter in the example below with either Body or Form would work regardless—Form is a class that inherits directly from Body. That is, FastAPI would still expect the JSON string as form-data, not application/json, as in this case the request will have the body encoded using multipart/form-data). Thus, the exact same test.py examples and index.html template provided in Method 3 above could be used for testing the application example below as well.
app.py
from fastapi import FastAPI, File, Body, UploadFile, Request
from pydantic import BaseModel, model_validator
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
@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(data: Base = Body(...), files: List[UploadFile] = File(...)):
return {"JSON Payload": data, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
Method 5
Another solution would be to convert the file bytes into a base64-format string and add it to the JSON object, along with other data that you might want to send over to the server. However, I would not highly recommend using this approach for the reasons explained below—it has been added, though, to this answer as an alternative option, for the sake of completeness.
The reason I would not suggest using it is because encoding file(s) using base64 would essentially increase the size of the file, and hence, increase bandwidth utilization, as well as the time and resources (e.g., CPU usage) required to upload the file (especially, when the API is going to be used by multiple users at the same time), as base64 encoding and decoding would need to take place on client and server side, respectively (this approach could only be useful for very tiny images). As per MDN's documentation:
Each Base64 digit represents exactly 6 bits of data. So, three 8-bits
bytes of the input string/binary file (3×8 bits = 24 bits) can be
represented by four 6-bit Base64 digits (4×6 = 24 bits).
This means that the Base64 version of a string or file will be at
least 133% the size of its source (a ~33% increase). The increase
may be larger if the encoded data is small. For example, the string
"a" with length === 1 gets encoded to "YQ==" with length === 4
— a 300% increase.
Using this approach, which again I would not recommend for the reasons discussed above, you would need to make sure to define the endpoint with normal def, as base64.b64decode() performs a blocking operation that would block the event loop, and hence the entire server—have a look at this answer for more details. Otherwise, to use async def endpoint, you should execute the decoding function in an external ThreadPool or ProcessPool (again, see this answer on how to do that), as well as use aiofiles to write the file to disk (see this answer as well).
The example below provides client test examples in Python requests and JavaScript as well.
app.py
from fastapi import FastAPI, Request, HTTPException
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from fastapi.templating import Jinja2Templates
import base64
import binascii
app = FastAPI()
templates = Jinja2Templates(directory='templates')
class Bas64File(BaseModel):
filename: str
owner: str
bas64_str: str
@app.post('/submit')
def submit(files: List[Bas64File]):
for file in files:
try:
contents = base64.b64decode(file.bas64_str.encode('utf-8'))
with open(file.filename, 'wb') as f:
f.write(contents)
except base64.binascii.Error as e:
raise HTTPException(
400, detail='There was an error decoding the base64 string'
)
except Exception:
raise HTTPException(
500, detail='There was an error uploading the file(s)'
)
return {'Filenames': [file.filename for file in files]}
@app.get('/', response_class=HTMLResponse)
async def main(request: Request):
return templates.TemplateResponse('index.html', {'request': request})
Test using Python requests
test.py
import requests
import os
import glob
import base64
url = 'http://127.0.0.1:8000/submit'
paths = glob.glob('files/*', recursive=True)
payload = []
for p in paths:
with open(p, 'rb') as f:
bas64_str = base64.b64encode(f.read()).decode('utf-8')
payload.append({'filename': os.path.basename(p), 'owner': 'me', 'bas64_str': bas64_str})
resp = requests.post(url=url, json=payload)
print(resp.json())
Test using Fetch API
templates/index.html
<input type="file" id="fileInput" onchange="base64Handler()" multiple><br>
<script>
async function base64Handler() {
var fileInput = document.getElementById('fileInput');
var payload = [];
for (const file of fileInput.files) {
var dict = {};
dict.filename = file.name;
dict.owner = 'me';
base64String = await this.toBase64(file);
dict.bas64_str = base64String.replace("data:", "").replace(/^.+,/, "");
payload.push(dict);
}
uploadFiles(payload);
}
function toBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
};
function uploadFiles(payload) {
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
</script>
DataConfigurationclass.