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.
3
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.
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
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 (which are mainly based on some of the following methods).
Method 1
As described here, one can define files and form fileds at the same time using File
and Form
. Below is a working example. 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 alternatively return a simple HTMLResponse
. See this answer as well, if you are looking for a JavaScript Fetch API solution instead.
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, that is, the Base
model). Also, please note that one should never pass sensitive data through the query string, as this poses a serious security risk—please have a look at this answer for more details on that topic.
When returning a Pydantic model instance (in this case, that is base
) from a FastAPI endpoint (e.g., /submit
endpoint below), 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})
Again, you can test it using the template below, which, this time, 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
.
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, to test the API, you could also use Swagger UI, or Python requests
, as shown in the example below. Note that the data should now be passed to the params
(not the data
) argument, of the requests.post()
method, as the data are now sent as query
parameters, not form-data
in the request body, 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 single parameter (of type Form
) in the form of a JSON string. 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 parameters (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, for example, data: str = Form(...)
). Then, create a new parameter (e.g., base
) in your endpoint, using Depends()
and passing 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, for instance, 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 in this section.
In this case, the dependency function 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 (i.e., Base
model, in this case) is assigned to the parameter in the endpoint, which could be used as desired. 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:
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 documentation (see this answer for more details 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 input data against a specific 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, you could use the below:
# ...
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 (as shown here):
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"
withlength === 1
gets encoded to"YQ=="
withlength === 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>
5
You can’t mix form-data with json.
Per FastAPI documentation:
Warning:
You can declare multipleFile
andForm
parameters in a path operation, but you can’t also declareBody
fields that you expect to receive as JSON, as the request will have the body encoded usingmultipart/form-data
instead ofapplication/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
3
If you are using pydantic v2
:
import json
@app.post(/endpoint)
async def endpoint(file: UploadFile, payload: A)
class A(BaseModel):
attr: str
@model_validator(mode="before")
@classmethod
def to_py_dict(cls, data):
return json.loads(data)
Your request shall be a multipart/form-data, the payload key’s value will be a string in JSON’s format, and when it reaches the model’s serialization stage, the @model_validator will execute before that, and then you can transform the value into a python’s dict and return it to the serialization.
0
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
Not an alternative, but an addendum to Chris’s generous response above /a/70640522/20479, that helped me with my code, but only after some debugging because I’d changed some variable names.
So the gotcha is that the mapping to parameter names and form field names is key.
Take Chris’s excellent method 3, which I ended up using. The last example of plain json exemplifies what confused me:
In the FastAPI service, we have:
def checker(data: str = Form(...)):
...
@app.post("/submit")
def submit(data: dict = Depends(checker), files: List[UploadFile] = File(...)):
pass
and in the client code we see:
files = [('files', open('test_files/a.txt', 'rb')), ('files', ...
data = {'data': '{"name":...}
resp = requests.post(url=url, data=data, files=files)
Note that the terms data
and files
appear no fewer than 6 times each in the code I excerpted. We also see That’s what tripped me up with hard-to-resolve errors including info like [{'loc': ('body', 'data'), 'msg': 'field required', 'type': 'value_error.missing'}]
(which I’ve learnt to read as “there’s a missing form field named ‘data’ expected in the ‘body’ of this request)
So the catch is that the first data in submit
is the function parameter and it’s name doesn’t have to match anything other than references to it below in the function (which are left to the imagination). This parameter is arbitrary and could be foo
here.
The one in def checker(data:...
, however, is crucial. It can have any name, but must be used in the request, specifically as the dict key in the form. (Read that again).
i.e. it matches the second ‘data’ in this line:
data = {'data': '{"name":...}
This is because checker
is a FastAPI dependency function, so its parameters are used in place of parameters in path operation function. (That’s the whole point of dependencies: reusing parameter sets instead of repeating them).
See the details here: https://fastapi.tiangolo.com/tutorial/dependencies/. The phrase that helped me was down the page where it says:
And it has the same shape and structure that all your path operation functions have.
You can think of it as a path operation function without the “decorator” (without the @app.get(“/some-path”)).
(Note that def submit...
is an example of a “path operation function”)
Meanwhile, the first data
in the client line
resp = requests.post(url=url, data=data, files=files)
is required by the requests post
method (so if you change that one, you’ll find out soon).
Similarly the only values of files
that have to match are the ones in the dict created in the client and the parameter name in the function. The rest are either required parameters for the requests post
function, or arbitrary choices.
Don’t get me wrong – calling the arbitrary variable by the same name as the parameter to which you assign it, is very pythonic – just that it tripped me up in understanding Chris’s answer.
To make it clearer, I’m transcribing my excerpt below, replacing the word “data” where I can. (And adding an assertion and a write..)
Service:
def checker(foo: str = Form(...)):
return "dingo"
...
@app.post("/submit")
def submit(quux: dict = Depends(checker), bananas: List[UploadFile] = File(...)):
assert quux == "dingo" # quux assigned to return value of checker
# write bananas to local files:
and in the client:
apples = [('bananas', open('test_files/a.txt', 'rb')), ('bananas', ...
baz = {'foo': { 'name': '...'} ... }
resp = requests.post(url=url, data=baz, files=apples)
Now there’s only one ‘data’ and its required by requests (and by the same method in httpx, which is what I’m using)
The biggest two gotchas here are:
-
the dependency function param
checker(foo
must be provided in the form data{'foo': {...
-
the client must provide names matching the form fields as keys in the dict / json of the request body. Look carefully at the 2 appearances of ‘foo’ and the 4 of ‘bananas’ in my code.
0