I am creating a registration API in FastAPI using Pydantic models. During registration, users need to input their files, user information, and school information. Since users should be able to input multiple schools, I am considering passing the school information as a list. Based on my research, it seems that uploading a file and sending data in JSON format are not supported in the same API. Therefore, I found out that I need to send the data in form_data instead of JSON format(How to add both file and JSON body in a FastAPI POST request?). I was able to convert and send user information as form data, but I'm not sure how to pass the list of school information. What I've tried so far is as follows.
I used a class method to receive User information as form data.
class User(BaseModel):
name: str
password: str
email: str
@classmethod
def as_form(cls, name: str = Form(...), password: str= Form(...),
email: str= Form(...)) -> 'User':
return cls(name=first_name, password=password, email=email)
This is the model I want to receive as a list.
class School(BaseModel):
school: str
country: str
and This is api.
@router.post("/signup", status_code=status.HTTP_201_CREATED)
async def signup(hospitals: List[School] = Form(...),
user: User = Depends(UserIn.as_form),
file: UploadFile = File(),session: Session = Depends(db.session)):
I tried
schools: Annotated[Json[School], Form]
I also tried creating an as_form class method for the School model, just like I did for the User model, in order to pass it as a list like this. However, it didn't work for either. Additionally, it seems that passing form data as a list is not a commonly used approach.
schools: List[School] = Depends(School.as_form)
I'm not sure how to approach this. I considered receiving data in JSON format and parsing it on the server, but in that case, how should I define the Pydantic models? I would like to know the most ideal approach in my current situation. Can you help me?
One way to solve this would be to have two separate List
parameters of Form
type, one for schools
and another one for countries
. For instance:
@router.post("/signup")
async def signup(schools: List[str] = Form(...), countries: List[str] = Form(...))
pass
However, since such a solution would require extra work in the backend to combine the two lists into one etc.,—as well as things would become more complicated, if more lists needed to be added—the following solutions are also given below.
Solutions 1 and 2 provided below are heavily based on Methods 3 and 4 of this answer, respectively. Hence, please refer to that answer for further details and explanation. Both the solutions below demonstrate how to upload both File
(s) and a List
of JSON data in FastAPI using a single POST
request. Client examples in Python requests
and JavaScript Fetch API are also provided below. Related answers can also be found here and here.
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: List[str] = Form(...)):
models = []
try:
for d in data:
models.append(Base.model_validate_json(d))
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return models
@app.post("/submit")
def submit(models: List[Base] = Depends(checker), files: List[UploadFile] = File(...)):
return {"JSON Payload ": models, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
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: List[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})
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'))]
items = ['{"name": "foo", "point": 0.11, "is_accepted": false}', '{"name": "bar", "point": 0.42, "is_accepted": true}']
data = {'data': items}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
templates/index.html
<!DOCTYPE html>
<html>
<body>
<input type="file" id="fileInput" name="file" onchange="reset()" multiple><br>
<input type="button" value="Submit using fetch" 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();
var items = [];
items[0] = JSON.stringify({"name": "foo", "point": 0.11, "is_accepted": false});
items[1] = JSON.stringify({"name": "bar", "point": 0.42, "is_accepted": true});
for (const i of items)
formData.append("data", i);
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";
}
}
</script>
</body>
</html>