Theodore Williams
Theodore Williams

Reputation: 65

Can I make a FastAPI endpoint receive json OR a file

I am trying to make a FastAPI endpoint where a user can upload documents in json format or in a gzip file format. I can get the endpoint to receive data from these two methods alone/separately, but not together in one endpoint/function. Is there a way to make the same FastAPI endpoint receive either json or a file?

Example with json:

from fastapi import FastAPI
from pydantic import BaseModel


class Document(BaseModel):
    words: str


app = FastAPI()


@app.post("/document/")
async def create_item(document_json: Document):
    return document_json

Example with file:

from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.gzip import GZipMiddleware


app = FastAPI()
app.add_middleware(GZipMiddleware)


@app.post("/document/")
async def create_item(document_gzip: UploadFile = File(...)):
    return document_gzip

Not working example with either-or:

from typing import Optional
from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.gzip import GZipMiddleware
from pydantic import BaseModel


class Document(BaseModel):
    words: Optional[str] = None


app = FastAPI()
app.add_middleware(GZipMiddleware)


@app.post("/document/")
async def create_item(
    document_json: Document, document_gzip: Optional[UploadFile] = File(None)
):
    return document_json, document_gzip

Upvotes: 4

Views: 5250

Answers (3)

Victor Augusto
Victor Augusto

Reputation: 2426

My working sample based on previous response:

async def invocations(input: Request, input_file: Optional[UploadFile] = File(None)):

    if input_file is None:
        input_json = await input.json()

    if input_file is not None:
        input_df = pd.read_csv(BytesIO(input_file.file.read()))
        return models.get("model").batch_predict(input_df=input_df)

Request with json:

curl --location --request POST 'http://0.0.0.0:8080/invocations' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_attr": 10,
    "second_attr": "all good"
}'

Request with file:

curl --location --request POST 'http://0.0.0.0:8080/invocations' \
--form 'input_file=@"/absolute-path-to-file/some-json-file.csv"'

Downside is that generated docs are not reflecting this behavior (have not tried to fix this yet): enter image description here

Upvotes: 1

Bhanu Teja
Bhanu Teja

Reputation: 41

This can't be done.

You can declare multiple File and 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 multipart/form-data instead of application/json.

This is not a limitation of FastAPI, it's part of the HTTP protocol.

You can, however, use Form(None) as a workaround to attach an extra List or similar types as form-data

Here is the snippet I used for this use case

@extract_operations_router.post("/runcompletepipe")
async def runcompletepipeline(urls: Optional[List[HttpUrl]] = Form(None),files : Optional[List[UploadFile]] = File(None)):
    print(urls,files)
    # for file in files:
    #     print(file.filename)
    return('qapairslist')

Upvotes: 0

lsabi
lsabi

Reputation: 4476

In your "not working example" with both options, made the json document mandatory. That is, the parameter has to be provided. I guess that posting the json document but not the file works, but the other way around fails. Am I right?

Anyways, the correct code should look as follows (note: I did not test it):

@app.post("/document/")
async def create_item(
    document_json: Document = None, document_gzip: Optional[UploadFile] = File(None)
):
    # Check that either are not none
    return document_json, document_gzip

EDIT based on your comment:

It could be due to how Fastapi processes the request for you. Since you specify both JSON and File from the body, it could be that Fastapi uses just the last (it's only my assumption, could be interesting exploring that). So it will always consider all parameters (except for GET) as file objects and check in the request's body for files.

You could try playing around with the raw Request object in order to check the body for any JSON.

Below a potential example what it could look like. Since Fastapi is based on Starlette, several features are shared between the two. The Request object is among those. Here are the docs with more info (in case you need it)

https://www.starlette.io/requests/

Below the updated example, which I did not test, but should work

@app.post("/document/")
    async def create_item(
        req: Request, document_gzip: Optional[UploadFile] = File(None)
    ):
        if document_gzip is None:
            # Maybe use a try except (i.e. try-catch) block... just in case
            document_json = await req.json()

        # Check that either are not none
        return document_json, document_gzip

Upvotes: 2

Related Questions