gr0gu3
gr0gu3

Reputation: 815

How to upload both file and JSON data using FastAPI?

This is my Pydantic model:

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

This is the endpoint:

def create_base(
    base: Base = Form(...),
    file: List[UploadFile] = File(...)
):
...

I'm trying to send a request via multipart form, but i'm getting the error:

{
    "detail": [
        {
            "loc": [
                "body",
                "base"
            ],
            "msg": "value is not a valid dict",
            "type": "type_error.dict"
        }
    ]
} 

This is the payload of my request:

{
  "name": "string",
  "point": 10.0,
  "is_accepted": true
}

What am I doing wrong?

Upvotes: 4

Views: 9118

Answers (1)

Chris
Chris

Reputation: 34055

Update

Please have a look at this answer for more details and options.



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

So, as described here, one can define files and form fields at the same time using File and Form. 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")
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

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 through OpenAPI docs (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 parameterized 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(...)):
    received_data= base.dict()
    return {"JSON Payload ": received_data, "Uploaded 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:

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 use Swagger UI, or the Python requests example 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, params=payload, files=files)
print(resp.json())

Upvotes: 4

Related Questions