'How do I send list of dictionary as Body parameter in FastAPI?
In FastAPI to pass a 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 to my request. I could not find a way to define a 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 with this?
DataModelOut
class:
class DataModelOut(BaseModel):
message: str = ""
id: str = ""
input_data: dict = None
result: List[dict] = []
statusCode: int
Solution 1:[1]
I think you should add a config class with orm_mode
set to True in your Schema/Model class
class DataModelOut(BaseModel):
message: str = ""
id: str = ""
input_data: dict = None
result: List[dict] = []
statusCode: int
class Config:
orm_mode=True
Solution 2:[2]
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, Form, 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 = Form(...), 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"]}
data = {'data': json.dumps(data_dict)}
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.
Solution 3:[3]
The problem directly comes from response_model, and your returning values, let's say i have a app like this
class Example(BaseModel):
name: str
@app.post("/", response_model=Example)
async def example(value: int):
return value
Now i'm sending a request to this
pydantic.error_wrappers.ValidationError: 1 validation error for Example
response
value is not a valid dict (type=type_error.dict)
The error is same as yours. Even if i send the same parameters it will be raising the same error
class Example(BaseModel):
name: int
other: int
@app.post("/", response_model=Example)
async def example(name: int, other: int):
return name
Out: value is not a valid dict (type=type_error.dict)
But if i declare the query parameter like this(best practice from docs) it 'll work just fine.
class Example(BaseModel):
name: int
other: int
@app.post("/", response_model=Example)
async def example(ex: Example = Body(...)):
return ex
Out: {
"name": 0,
"other": 0
}
In your case you can create two seperate models, DataModelIn
and DataModelOut
,
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]
@app.post("/", response_model=DataModelOut)
async def create_policy_details(data: DataModelIn = Body(...)):
return {"input_data":data,
"statusCode":1}
Now i'm sending a request to this
Out: {
"message": "",
"id": "",
"input_data": {
"countryId": "30",
"policyDetails": [
{
"some": "details"
}
],
"leaveTypeId": "string",
"branchIds": [
"string"
],
"cityIds": [
"string"
]
},
"result": [],
"statusCode": 1
}
It works like a charm. You can also use response_model_exclude_unset=True
parameter to discard message
and id
from response,also check this out
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | davidsbro |
Solution 2 | |
Solution 3 |