How to add both file and JSON body in a FastAPI POST request?

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.

Method 1

As described here, one can define files and form data at the same time using File and Form fields. Below is a working example:

app.py

from fastapi import Form, File, UploadFile, Request, FastAPI
from typing import List
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.post("/submit")
async def submit(name: str = Form(...), point: float = Form(...), is_accepted: bool  = Form(...), files: List[UploadFile] = File(...)):
        return {"JSON Payload ": {"name": name, "point": point, "is_accepted": is_accepted}, "Filenames": [file.filename for file in files]}

@app.get("/", response_class=HTMLResponse)
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

You can still use a Pydantic model with the above method if you wish, by creating a new instance of the Pydantic model using the received data, as
shown below:

class Base(BaseModel):
    name: str
    point: Optional[float] = None
    is_accepted: Optional[bool] = False    @app.post("/submit")  

@app.post("/submit")
   ...
   return {"JSON Payload ": Base(name=name, point=point, is_accepted=is_accepted), "Filenames": [file.filename for
file in files]} 

You can test it by accessing the template below at http://127.0.0.1:8000.

templates/index.html

<!DOCTYPE html>
<html>
   <body>
      <form method="post" action="http://127.0.0.1:8000/submit"  enctype="multipart/form-data">
         name : <input type="text" name="name" value="foo"><br>
         point : <input type="text" name="point" value=0.134><br>
         is_accepted : <input type="text" name="is_accepted" value=True><br>    
         <label for="file">Choose files to upload</label>
         <input type="file" id="files" name="files" multiple>
         <input type="submit" value="submit">
      </form>
   </body>
</html>

You can also test it using OpenAPI at http://127.0.0.1:8000/docs, or Python requests, as shown below:

test.py

import requests

url="http://127.0.0.1:8000/submit"
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
payload ={"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, data=payload, files=files) 
print(resp.json())

Method 2

One can use Pydantic models, along with Dependencies to inform the “submit” route (in the case below) that the parameterised variable base depends on the Base class. Please note, this method expects the base data as query (not body) parameters (which are then converted into an equivalent JSON payload using .dict() method) and the Files as multipart/form-data in the body.

app.py

from fastapi import Form, File, UploadFile, Request, FastAPI, Depends
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Optional
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

class Base(BaseModel):
    name: str
    point: Optional[float] = None
    is_accepted: Optional[bool] = False

@app.post("/submit")
async def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
    received_data= base.dict()
    return {"JSON Payload ": received_data, "Filenames": [file.filename for file in files]}
 
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

Again, you can test it with the template below (which uses Javascript to modify the action attribute of the form, in order to pass the form data as query params to the URL).

templates/index.html

<!DOCTYPE html>
<html>
   <body>
      <form method="post" id="myForm" onclick="transformFormData();" enctype="multipart/form-data">
         name : <input type="text" name="name" value="foo"><br>
         point : <input type="text" name="point" value=0.134><br>
         is_accepted : <input type="text" name="is_accepted" value=True><br>    
         <label for="file">Choose files to upload</label>
         <input type="file" id="files" name="files" multiple>
         <input type="submit" value="submit">
      </form>
      <script>
         function transformFormData(){
            var myForm = document.getElementById('myForm');
            var qs = new URLSearchParams(new FormData(myForm)).toString();
            myForm.action = 'http://127.0.0.1:8000/submit?'+qs;
         }
      </script>
   </body>
</html>

As mentioned earlier you can also use OpenAPI docs, or Python requests, as shown in the example below. Note: this time params=payload is used, as the parameters are query params, not body (data) params.

test.py

import requests

url="http://127.0.0.1:8000/submit"
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
payload ={"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, params=payload, files=files)
print(resp.json())

Method 3

Another option would be to pass the body data as a single parameter (of type Form) in the form of a JSON string. On server side, you can create a dependency function, where you parse the data using parse_raw method and validate the data against the corresponding model. If ValidationError is raised, an HTTP_422_UNPROCESSABLE_ENTITY error is sent back to the client, including the error message. Example is given below:

app.py

from fastapi import FastAPI, status, Form, UploadFile, File, Depends, Request
from pydantic import BaseModel, ValidationError
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

app = FastAPI()
templates = Jinja2Templates(directory="templates")

class Base(BaseModel):
    name: str
    point: Optional[float] = None
    is_accepted: Optional[bool] = False

async def checker(data: str = Form(...)):
    try:
        model = Base.parse_raw(data)
    except ValidationError as e:
        raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
        
    return model
    
@app.post("/submit")
async def submit(model: Base = Depends(checker), files: List[UploadFile] = File(...)):
        return {"JSON Payload ": model, "Filenames": [file.filename for file in files]}

@app.get("/", response_class=HTMLResponse)
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

test.py

Note that in JSON, boolean values are lower case (i.e., true and false), whereas in Python they are capitalised (True and False).

import requests

url="http://127.0.0.1:8000/submit"
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': '{"name": "foo", "point": 0.13, "is_accepted": false}'}
resp = requests.post(url=url, data=data, files=files) 
print(resp.json())

Or, if you prefer:

import requests
import json

url="http://127.0.0.1:8000/submit"
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': json.dumps({"name": "foo", "point": 0.13, "is_accepted": False})}
resp = requests.post(url=url, data=data, files=files) 
print(resp.json())

Test using Fetch API or Axios

templates/index.html

<!DOCTYPE html>
<html>
   <head>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js"></script>
   </head>
   <body>
      <input type="file" id="fileInput" name="file" multiple><br>
      <input type="button" value="Submit using fetch" onclick="submitUsingFetch()">
      <input type="button" value="Submit using axios" onclick="submitUsingAxios()">
      <script>
         function submitUsingFetch() {
             var fileInput = document.getElementById('fileInput');
             if (fileInput.files[0]) {
                var formData = new FormData();
                formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
                for (const file of fileInput.files)
                    formData.append('files', file);
                    
                 fetch('/submit', {
                       method: 'POST',
                       body: formData,
                     })
                     .then(response => {
                       console.log(response);
                     })
                     .catch(error => {
                       console.error(error);
                     });
             }
         }
         
         function submitUsingAxios() {
             var fileInput = document.getElementById('fileInput');
             if (fileInput.files[0]) {
                var formData = new FormData();
                formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
                for (const file of fileInput.files)
                    formData.append('files', file);
                    
                 axios({
                         method: 'POST',
                         url: '/submit',
                         data: formData,
                     })
                     .then(response => {
                       console.log(response);
                     })
                     .catch(error => {
                       console.error(error);
                     });
             }
         }
      </script>
   </body>
</html>

Method 4

A likely preferable method comes from the discussion here, and incorporates a custom class with a classmethod used to transform a given JSON string into a Python dictionary, which is then used for validation against the Pydantic model. Similar to Method 3 above, the input data should be passed as a single Form parameter in the form of JSON string. Thus, the same test.py file(s) and index.html template from the previous method can be used for testing the below.

app.py

from fastapi import FastAPI, File, Form, UploadFile, Request
from pydantic import BaseModel
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import json

app = FastAPI()
templates = Jinja2Templates(directory="templates")

class Base(BaseModel):
    name: str
    point: Optional[float] = None
    is_accepted: Optional[bool] = False

    @classmethod
    def __get_validators__(cls):
        yield cls.validate_to_json

    @classmethod
    def validate_to_json(cls, value):
        if isinstance(value, str):
            return cls(**json.loads(value))
        return value
    
@app.post("/submit")
def submit(data: Base = Form(...), files: List[UploadFile] = File(...)):
        return {"JSON Payload ": data, "Filenames": [file.filename for file in files]}
        
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

Leave a Comment