4

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?

Chris
  • 18,724
  • 6
  • 46
  • 80
gr0gu3
  • 555
  • 1
  • 6
  • 11
  • Are you sending form data or a JSON body? Your example seems to indicate you're sending JSON, but you're using `Form` and mentioning multipart form-data. – MatsLindh Dec 30 '21 at 21:15
  • Yeah, I want to send a JSON to this specific object (Base) but in a multipart form-data. Is this possible? I need to send this specific boundary (base) in json and also the files. – gr0gu3 Dec 31 '21 at 00:59
  • 1
    Does this answer your question? [How to add both file and JSON body in a FastAPI POST request?](https://stackoverflow.com/questions/65504438/how-to-add-both-file-and-json-body-in-a-fastapi-post-request) – Gino Mempin Sep 10 '22 at 05:21

1 Answers1

4

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 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(...)):
    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())
Chris
  • 18,724
  • 6
  • 46
  • 80
  • If the answer is already provided in another existing Q&A pair, [please vote to close as duplicate](https://stackoverflow.com/help/duplicates) instead, rather than copying and fragmenting the answers into multiple similar posts. – Gino Mempin Sep 10 '22 at 05:23