Specifically, I want the below example to work:
from typing import List
from pydantic import BaseModel
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
class DataConfiguration(BaseModel):
textColumnNames: List[str]
idColumn: str
#app.post("/data")
async def data(dataConfiguration: DataConfiguration,
csvFile: UploadFile = File(...)):
pass
# read requested id and text columns from csvFile
If this is not the proper way for a POST request, please let me know how to select the required columns from an uploaded CSV file in FastAPI.
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 first—if you haven't already—since uploaded files are sent as "form data". For instance:
pip install python-multipart
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. If you have 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 create a custom dependency class.
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. If your template does not include any Jinja code, you could alternatively return a simple HTMLResponse.
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 the interactive OpenAPI docs (provided by 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())
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")
def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
return {
"JSON Payload ": base.dict(),
"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 using the template below, which, this time, 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 Swagger UI, or Python requests, as shown in the example below. Note that this time, the payload is passed to the params parameter of requests.post(), as you submit query parameters, not form-data (body) params, which was the case in the previous method.
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
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")
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})
In case you had multiple models and would like to avoid creating a checker function for each model, you could instead create a checker class, as described in the documentation, and have a dictionary of your models that you can use to look up for a model to parse. Example:
# ...
models = {"base": Base, "other": SomeOtherModel}
class DataChecker:
def __init__(self, name: str):
self.name = name
def __call__(self, data: str = Form(...)):
try:
model = models[self.name].parse_raw(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return model
base_checker = DataChecker("base")
other_checker = DataChecker("other")
#app.post("/submit")
def submit(model: Base = Depends(base_checker), files: List[UploadFile] = File(...)):
# ...
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 capitalised 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())
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 further 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 (defining the parameter with Body type would also work and still expect the JSON string as form data, as in this case the data comes encoded as multipart/form-data). 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, Body, 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 = Body(...), 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})
You can't mix form-data with json.
Per FastAPI documentation:
Warning:
You can declare multiple File and 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 multipart/form-data instead of application/json.
This is not a limitation of FastAPI, it's part of the HTTP protocol.
You can, however, use Form(...) as a workaround to attach extra string as form-data:
from typing import List
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
#app.post("/data")
async def data(textColumnNames: List[str] = Form(...),
idColumn: str = Form(...),
csvFile: UploadFile = File(...)):
pass
I went with the very elegant Method3 from #Chris (originally proposed from #M.Winkwns). However, I modified it slightly to work with any Pydantic model:
from typing import Type, TypeVar
from pydantic import BaseModel, ValidationError
from fastapi import Form
Serialized = TypeVar("Serialized", bound=BaseModel)
def form_json_deserializer(schema: Type[Serialized], data: str = Form(...)) -> Serialized:
"""
Helper to serialize request data not automatically included in an application/json body but
within somewhere else like a form parameter. This makes an assumption that the form parameter with JSON data is called 'data'
:param schema: Pydantic model to serialize into
:param data: raw str data representing the Pydantic model
:raises ValidationError: if there are errors parsing the given 'data' into the given 'schema'
"""
try:
return schema.parse_raw(data)
except ValidationError as e
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
When you use it in an endpoint you can then use functools.partial to bind the specific Pydantic model:
import functools
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
#app.post("/upload")
async def upload(
data: Base = Depends(functools.partial(form_json_deserializer, Base)),
files: Sequence[UploadFile] = File(...)
) -> Base:
return data
As stated by #Chris (and just for completeness):
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. (But 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.
Since his Method1 wasn't an option and Method2 can't work for deeply nested datatypes I came up with a different solution:
Simply convert your datatype to a string/json and call pydantics parse_raw function
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
#app.post("/submit")
async def submit(base: str = Form(...), files: List[UploadFile] = File(...)):
try:
model = Base.parse_raw(base)
except pydantic.ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
) from e
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
Example using pythantic models for cleaner documentation. The file is encoded to base64 any other logic can be applied.
class BaseTestUser(BaseModel):
name: str
image_1920: str
class UpdateUserEncodeFile(BaseTestUser):
def __init__(self, name: str = Form(...), image_1920: UploadFile = File(...)):
super().__init__(name=name, image_1920=base64.b64encode(image_1920.file.read()))
#routers
#router.put("/users/{id}/encoded", status_code=status.HTTP_200_OK)
def user_update_encode(id: int, user:UpdateUserEncodeFile=Depends()):
return user
Related
Specifically, I want the below example to work:
from typing import List
from pydantic import BaseModel
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
class DataConfiguration(BaseModel):
textColumnNames: List[str]
idColumn: str
#app.post("/data")
async def data(dataConfiguration: DataConfiguration,
csvFile: UploadFile = File(...)):
pass
# read requested id and text columns from csvFile
If this is not the proper way for a POST request, please let me know how to select the required columns from an uploaded CSV file in FastAPI.
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 first—if you haven't already—since uploaded files are sent as "form data". For instance:
pip install python-multipart
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. If you have 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 create a custom dependency class.
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. If your template does not include any Jinja code, you could alternatively return a simple HTMLResponse.
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 the interactive OpenAPI docs (provided by 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())
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")
def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
return {
"JSON Payload ": base.dict(),
"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 using the template below, which, this time, 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 Swagger UI, or Python requests, as shown in the example below. Note that this time, the payload is passed to the params parameter of requests.post(), as you submit query parameters, not form-data (body) params, which was the case in the previous method.
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
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")
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})
In case you had multiple models and would like to avoid creating a checker function for each model, you could instead create a checker class, as described in the documentation, and have a dictionary of your models that you can use to look up for a model to parse. Example:
# ...
models = {"base": Base, "other": SomeOtherModel}
class DataChecker:
def __init__(self, name: str):
self.name = name
def __call__(self, data: str = Form(...)):
try:
model = models[self.name].parse_raw(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return model
base_checker = DataChecker("base")
other_checker = DataChecker("other")
#app.post("/submit")
def submit(model: Base = Depends(base_checker), files: List[UploadFile] = File(...)):
# ...
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 capitalised 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())
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 further 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 (defining the parameter with Body type would also work and still expect the JSON string as form data, as in this case the data comes encoded as multipart/form-data). 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, Body, 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 = Body(...), 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})
You can't mix form-data with json.
Per FastAPI documentation:
Warning:
You can declare multiple File and 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 multipart/form-data instead of application/json.
This is not a limitation of FastAPI, it's part of the HTTP protocol.
You can, however, use Form(...) as a workaround to attach extra string as form-data:
from typing import List
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
#app.post("/data")
async def data(textColumnNames: List[str] = Form(...),
idColumn: str = Form(...),
csvFile: UploadFile = File(...)):
pass
I went with the very elegant Method3 from #Chris (originally proposed from #M.Winkwns). However, I modified it slightly to work with any Pydantic model:
from typing import Type, TypeVar
from pydantic import BaseModel, ValidationError
from fastapi import Form
Serialized = TypeVar("Serialized", bound=BaseModel)
def form_json_deserializer(schema: Type[Serialized], data: str = Form(...)) -> Serialized:
"""
Helper to serialize request data not automatically included in an application/json body but
within somewhere else like a form parameter. This makes an assumption that the form parameter with JSON data is called 'data'
:param schema: Pydantic model to serialize into
:param data: raw str data representing the Pydantic model
:raises ValidationError: if there are errors parsing the given 'data' into the given 'schema'
"""
try:
return schema.parse_raw(data)
except ValidationError as e
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
When you use it in an endpoint you can then use functools.partial to bind the specific Pydantic model:
import functools
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
#app.post("/upload")
async def upload(
data: Base = Depends(functools.partial(form_json_deserializer, Base)),
files: Sequence[UploadFile] = File(...)
) -> Base:
return data
As stated by #Chris (and just for completeness):
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. (But 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.
Since his Method1 wasn't an option and Method2 can't work for deeply nested datatypes I came up with a different solution:
Simply convert your datatype to a string/json and call pydantics parse_raw function
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
#app.post("/submit")
async def submit(base: str = Form(...), files: List[UploadFile] = File(...)):
try:
model = Base.parse_raw(base)
except pydantic.ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
) from e
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
Example using pythantic models for cleaner documentation. The file is encoded to base64 any other logic can be applied.
class BaseTestUser(BaseModel):
name: str
image_1920: str
class UpdateUserEncodeFile(BaseTestUser):
def __init__(self, name: str = Form(...), image_1920: UploadFile = File(...)):
super().__init__(name=name, image_1920=base64.b64encode(image_1920.file.read()))
#routers
#router.put("/users/{id}/encoded", status_code=status.HTTP_200_OK)
def user_update_encode(id: int, user:UpdateUserEncodeFile=Depends()):
return user
I am trying to do a simple POST operation using FastAPI. I have created a basic structure using BaseModel, which has only two attributes, namely name and roll.
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
roll: int
app = FastAPI()
#app.post("/")
async def create_item(item: Item):
return item
if __name__ == '__main__':
uvicorn.run(app, port=8080, host='0.0.0.0')
I would like to post these data using this POST operation -
{"name":"XYZ", "roll":51}.
I know about the automatic documentation at http://localhost:8080/docs provided by Swagger UI (OpenAPI), which we can use to post data. But I wouldn't want to use it. What I would like is to directly post the data using the URL http://localhost:8080/ and would like to see the result in the browser itself, instead of seeing the result in Swaggger UI.
You would need to use a Javascript interface/library such as Fetch API, which allows you to send data in JSON format (example is given below). For submiting Form data instead, have a look at this answer, while for posting both Files and Form/JSON data, have a look at this answer.
For the frontend, you could use Jinja2Templates to render and return a TemplateResponse that includes your HTML/JS code, etc. You can use an HTML form to submit your data and then have the form-data converted into JSON, as described here. Otherwise, you could post your JSON data directly, as shown here—for example, body: JSON.stringify({name: "foo", roll: 1}).
app.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Item(BaseModel):
name: str
roll: int
#app.post("/")
async def create_item(item: Item):
return item
#app.get("/")
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
templates/index.html
<!DOCTYPE html>
<html>
<body>
<h1>Post JSON Data</h1>
<form method="post" id="myForm">
name : <input type="text" name="name" value="foo">
roll : <input type="number" name="roll" value="1">
<input type="button" value="Submit" onclick="submitForm()">
</form>
<div id="responseArea"></div>
<script>
function submitForm() {
var formElement = document.getElementById('myForm');
var data = new FormData(formElement);
fetch('/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.fromEntries(data))
})
.then(resp => resp.text()) // or, resp.json(), etc.
.then(data => {
document.getElementById("responseArea").innerHTML = data;
})
.catch(error => {
console.error(error);
});
}
</script>
</body>
</html>
I'm doing a feature where the user on their profile page makes changes (not related to the user model). Everything is implemented through static HTML templates. I need the user to click on the button and return to the same page (i.e., their profile page) after processing the request.
Html template
<td>Accept</td>
endpoints.py
#router.get('/invite/{pk}/decline')
async def decline_event_invite(
request: Request,
pk: int,
user_id: str = Depends(get_current_user),
service: InviteService = Depends(),
):
await service.invite_decline(pk)
...
--> here I want redirect to user profile page
return RedirectResponse('DYNAMIC URL WITH ARGS')
profile.py
#router.get('/{pk}')
async def user_profile(
request: Request,
pk: int,
service: UserService = Depends()
):
user = await service.get_user_info(pk)
events_invites = await service.get_user_events_invite_list(pk)
return templates.TemplateResponse(
'profile.html',
context=
{
'request': request,
'user': user,
'events_invites': events_invites,
}
)
But I can't find anywhere how to do a redirect similar to the logic that applies to templates. For example:
Sender
You can use url_for() function and pass the (**kwargs) path parameters.
import uvicorn
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
import urllib
from fastapi import APIRouter
router = APIRouter()
templates = Jinja2Templates(directory="templates")
#router.get('/invite/{pk}/decline')
def decline_event_invite(request: Request, pk: int):
redirect_url = request.url_for('user_profile', **{ 'pk' : pk})
return RedirectResponse(redirect_url)
#router.get('/{pk}')
def user_profile(request: Request, pk: int):
return templates.TemplateResponse("profile.html", {"request": request, "pk": pk})
if __name__ == "__main__":
uvicorn.run(router, host='127.0.0.1', port=8000, debug=True)
To add query params
In case you had to pass query params as well, you could use the following code (make sure to import urllib). Alternatively, you could use the CustomURLProcessor, as described in this and this answer (which pretty much follows the same approach).
If the endpoint expected query params, for example:
#router.get('/invite/{pk}/decline')
def decline_event_invite(request: Request, pk: int, message: str):
pass
you could use:
redirect_url = request.url_for('user_profile', pk=pk)
parsed = list(urllib.parse.urlparse(redirect_url))
parsed[4] = urllib.parse.urlencode({**{ 'message' : "Success!"}})
redirect_url = urllib.parse.urlunparse(parsed)
or even use:
message = 'Success!'
redirect_url = request.url_for('user_profile', pk=pk) + f'?message={message}'
Update
Another solution would be to use Starlette's starlette.datastructures.URL, which now provides a method to include_query_params. Example:
from starlette.datastructures import URL
redirect_url = URL(request.url_for('user_profile', pk=pk)).include_query_params(message="Success!")
Specifically, I want the below example to work:
from typing import List
from pydantic import BaseModel
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
class DataConfiguration(BaseModel):
textColumnNames: List[str]
idColumn: str
#app.post("/data")
async def data(dataConfiguration: DataConfiguration,
csvFile: UploadFile = File(...)):
pass
# read requested id and text columns from csvFile
If this is not the proper way for a POST request, please let me know how to select the required columns from an uploaded CSV file in FastAPI.
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 first—if you haven't already—since uploaded files are sent as "form data". For instance:
pip install python-multipart
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. If you have 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 create a custom dependency class.
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. If your template does not include any Jinja code, you could alternatively return a simple HTMLResponse.
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 the interactive OpenAPI docs (provided by 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())
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")
def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
return {
"JSON Payload ": base.dict(),
"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 using the template below, which, this time, 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 Swagger UI, or Python requests, as shown in the example below. Note that this time, the payload is passed to the params parameter of requests.post(), as you submit query parameters, not form-data (body) params, which was the case in the previous method.
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
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")
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})
In case you had multiple models and would like to avoid creating a checker function for each model, you could instead create a checker class, as described in the documentation, and have a dictionary of your models that you can use to look up for a model to parse. Example:
# ...
models = {"base": Base, "other": SomeOtherModel}
class DataChecker:
def __init__(self, name: str):
self.name = name
def __call__(self, data: str = Form(...)):
try:
model = models[self.name].parse_raw(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return model
base_checker = DataChecker("base")
other_checker = DataChecker("other")
#app.post("/submit")
def submit(model: Base = Depends(base_checker), files: List[UploadFile] = File(...)):
# ...
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 capitalised 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())
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 further 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 (defining the parameter with Body type would also work and still expect the JSON string as form data, as in this case the data comes encoded as multipart/form-data). 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, Body, 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 = Body(...), 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})
You can't mix form-data with json.
Per FastAPI documentation:
Warning:
You can declare multiple File and 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 multipart/form-data instead of application/json.
This is not a limitation of FastAPI, it's part of the HTTP protocol.
You can, however, use Form(...) as a workaround to attach extra string as form-data:
from typing import List
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
#app.post("/data")
async def data(textColumnNames: List[str] = Form(...),
idColumn: str = Form(...),
csvFile: UploadFile = File(...)):
pass
I went with the very elegant Method3 from #Chris (originally proposed from #M.Winkwns). However, I modified it slightly to work with any Pydantic model:
from typing import Type, TypeVar
from pydantic import BaseModel, ValidationError
from fastapi import Form
Serialized = TypeVar("Serialized", bound=BaseModel)
def form_json_deserializer(schema: Type[Serialized], data: str = Form(...)) -> Serialized:
"""
Helper to serialize request data not automatically included in an application/json body but
within somewhere else like a form parameter. This makes an assumption that the form parameter with JSON data is called 'data'
:param schema: Pydantic model to serialize into
:param data: raw str data representing the Pydantic model
:raises ValidationError: if there are errors parsing the given 'data' into the given 'schema'
"""
try:
return schema.parse_raw(data)
except ValidationError as e
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
When you use it in an endpoint you can then use functools.partial to bind the specific Pydantic model:
import functools
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
#app.post("/upload")
async def upload(
data: Base = Depends(functools.partial(form_json_deserializer, Base)),
files: Sequence[UploadFile] = File(...)
) -> Base:
return data
As stated by #Chris (and just for completeness):
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. (But 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.
Since his Method1 wasn't an option and Method2 can't work for deeply nested datatypes I came up with a different solution:
Simply convert your datatype to a string/json and call pydantics parse_raw function
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
#app.post("/submit")
async def submit(base: str = Form(...), files: List[UploadFile] = File(...)):
try:
model = Base.parse_raw(base)
except pydantic.ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
) from e
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
Example using pythantic models for cleaner documentation. The file is encoded to base64 any other logic can be applied.
class BaseTestUser(BaseModel):
name: str
image_1920: str
class UpdateUserEncodeFile(BaseTestUser):
def __init__(self, name: str = Form(...), image_1920: UploadFile = File(...)):
super().__init__(name=name, image_1920=base64.b64encode(image_1920.file.read()))
#routers
#router.put("/users/{id}/encoded", status_code=status.HTTP_200_OK)
def user_update_encode(id: int, user:UpdateUserEncodeFile=Depends()):
return user
FastAPI automatically generates a schema in the OpenAPI spec for UploadFile parameters.
For example, this code:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
#app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(..., description="The file")):
return {"filename": file.filename}
will generate this schema under components:schemas in the OpenAPI spec:
{
"Body_create_upload_file_uploadfile__post": {
"title": "Body_create_upload_file_uploadfile__post",
"required":["file"],
"type":"object",
"properties":{
"file": {"title": "File", "type": "string", "description": "The file","format":"binary"}
}
}
}
How can I explicitly specify the schema for UploadFiles (or at least its name)?
I have read FastAPIs docs and searched the issue tracker but found nothing.
I answered this over on FastAPI#1442, but just in case someone else stumbles upon this question here is a copy-and-paste from the post linked above:
After some investigation this is possible, but it requires some monkey patching. Using the example given here, the solution looks like so:
from fastapi import FastAPI, File, UploadFile
from typing import Callable
app = FastAPI()
#app.post("/files/")
async def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
#app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
return {"filename": file.filename}
def update_schema_name(app: FastAPI, function: Callable, name: str) -> None:
"""
Updates the Pydantic schema name for a FastAPI function that takes
in a fastapi.UploadFile = File(...) or bytes = File(...).
This is a known issue that was reported on FastAPI#1442 in which
the schema for file upload routes were auto-generated with no
customization options. This renames the auto-generated schema to
something more useful and clear.
Args:
app: The FastAPI application to modify.
function: The function object to modify.
name: The new name of the schema.
"""
for route in app.routes:
if route.endpoint is function:
route.body_field.type_.__name__ = name
break
update_schema_name(app, create_file, "CreateFileSchema")
update_schema_name(app, create_upload_file, "CreateUploadSchema")
You can edit the OpenAPI schema itself. I prefer to just move these schemas to the path (since they are unique to each path anyway):
from fastapi import FastAPI, File, UploadFile
from fastapi.openapi.utils import get_openapi
app = FastAPI()
#app.post("/uploadfile/")
async def create_upload_file(file1: UploadFile = File(...), file2: UploadFile = File(...)):
pass
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="Custom title",
version="2.5.0",
description="This is a very custom OpenAPI schema",
routes=app.routes,
)
# Move autogenerated Body_ schemas, see https://github.com/tiangolo/fastapi/issues/1442
for path in openapi_schema["paths"].values():
for method_data in path.values():
if "requestBody" in method_data:
for content_type, content in method_data["requestBody"]["content"].items():
if content_type == "multipart/form-data":
schema_name = content["schema"]["$ref"].lstrip("#/components/schemas/")
schema_data = openapi_schema["components"]["schemas"].pop(schema_name)
content["schema"] = schema_data
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi