Reputation: 65
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
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):
Upvotes: 1
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
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
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