Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How Do I send list of dictionary as Body parameter in FastAPI?

In FastAPI to pass list of dictionary, generally we will define a pydantic schema and will mention as

param: List[schema_model]

The issue i am facing is that i have files to attach in my request. I could not find a way to define schema and File Upload in router function. For that I am defining all the parameters(request body) as Body parameters like below.

@router.post("/", response_model=DataModelOut)
async def create_policy_details(request:Request,
    countryId: str = Body(...),
    policyDetails: List[dict] = Body(...),
    leaveTypeId: str = Body(...),
    branchIds: List[str] = Body(...),
    cityIds: List[str] = Body(...),
    files: List[UploadFile] = File(None)
    ):

when i send a request using form-data option of postman it is showing "0:value is not a valid dict" for policyDetails parameter. I am sending [{"name":"name1","department":"d1"}]. It is saying not a valid dict, Even though i send valid dict. Can any one help me on this? DataModelOut class

class DataModelOut(BaseModel):
    message: str = ""
    id: str = ""
    input_data: dict = None
    result: List[dict] = []
    statusCode: int
like image 991
samba Avatar asked Jul 27 '20 07:07

samba


People also ask

What are body parameters?

The body parameter is defined in the operation's parameters section and includes the following: in: body. schema that describes the body data type and structure. The data type is usually an object, but can also be a primitive (such as a string or number) or an array. Optional description .

Can path parameter be optional in FastAPI?

As query parameters are not a fixed part of a path, they can be optional and can have default values.

How do you pass query parameters in Python?

To send parameters in URL, write all parameter key:value pairs to a dictionary and send them as params argument to any of the GET, POST, PUT, HEAD, DELETE or OPTIONS request. then https://somewebsite.com/?param1=value1&param2=value2 would be our final url.


1 Answers

As per FastAPI documentation, when including Files or Form parameters, "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 (or multipart/form-data, if files are included) instead of application/json. Thus, you can't have both Form (and/or File) data together with JSON data. This is not a limitation of FastAPI, it's part of the HTTP protocol. Please have a look at this answer as well.

If you removed files: List[UploadFile] parameter from your endpoint, you would see that the request would go through without any errors. However, since you are declaring each parameter as Body, the request body would be encoded using application/json. You could check that through OpenAPI at http://127.0.0.1:8000/docs, for instance. When, however, files are included, although using Body fields (in this case, Form could be used as well, which is a class that inherits directly from Body - see here), the request body would be encoded using multipart/form-data. Hence, when declaring a parameter such as policyDetails: List[dict] = Body(...) (or even policyDetails: dict) that is essentially expecting JSON data, the error value is not a valid dict is raised (even though it is not that informative).

Therefore, your data, apart from files, could be sent as a stringified JSON, and on server side you could have a custom pydantic class that transforms the given JSON string into Python dictionary and validates it against the model, as described here. The files parameter should be defined separately from the model in your endpoint. Below is a working example demonstrating the aforementioned approach:

app.py

from fastapi import FastAPI, File, UploadFile, Body, status
from pydantic import BaseModel
from typing import Optional, List
import json

app = FastAPI()

class DataModelOut(BaseModel):
    message: str = ""
    id: str = ""
    input_data: dict = None
    result: List[dict] = []
    statusCode: int
    
class DataModelIn(BaseModel):
    countryId: str
    policyDetails: List[dict]
    leaveTypeId: str
    branchIds: List[str]
    cityIds: List[str]
    
    @classmethod
    def __get_validators__(cls):
        yield cls.validate_to_json

    @classmethod
    def validate_to_json(cls, value):
        if isinstance(value, str):
            return cls(**json.loads(value))
        return value
    
@app.post("/", response_model=DataModelOut)
def create_policy_details(data: DataModelIn = Body(...), files: Optional[List[UploadFile]] = File(None)):
        print("Files received: ", [file.filename for file in files])
        return {"input_data":data, "statusCode": status.HTTP_201_CREATED}

test.py

import requests

url = 'http://127.0.0.1:8000/'
files = [('files', open('a.txt', 'rb')), ('files', open('b.txt', 'rb'))]
data = {'data' : '{"countryId": "US", "policyDetails":  [{"name":"name1","department":"d1"}], "leaveTypeId": "some_id", "branchIds": ["b1", "b2"], "cityIds": ["c1", "c2"]}'}
resp = requests.post(url=url, data=data, files=files) 
print(resp.json())

or, if you prefer:

import requests
import json

url = 'http://127.0.0.1:8000/'
files = [('files', open('a.txt', 'rb')), ('files', open('b.txt', 'rb'))]
data_dict = {"countryId": "US", "policyDetails":  [{"name":"name1","department":"d1"}], "leaveTypeId": "some_id", "branchIds": ["b1", "b2"], "cityIds": ["c1", "c2"]}
json_str = json.dumps(data_dict)
data = {'data': json_str}
resp = requests.post(url=url, data=data, files=files) 
print(resp.json())

You could also test the app using OpenAPI at http://127.0.0.1:8000/docs.

like image 173
Chris Avatar answered Sep 30 '22 13:09

Chris