Home >Backend Development >Python Tutorial >How to accept both JSON and file uploads in a FastAPI POST request?

How to accept both JSON and file uploads in a FastAPI POST request?

Mary-Kate Olsen
Mary-Kate OlsenOriginal
2024-12-19 10:35:12447browse

How to accept both JSON and file uploads in a FastAPI POST request?

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

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.

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.

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 decalring Form fields, using a dependency class or Pydantic model instead.

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 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.

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">

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

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

Method 2

One could also use Pydantic models, along with Dependencies, to inform the /submit endpoint (in the example below) that the parameterised 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

pip install python-multipart

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

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})

The above is the detailed content of How to accept both JSON and file uploads in a FastAPI POST request?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn