20

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
Chris
  • 18,724
  • 6
  • 46
  • 80
samba
  • 869
  • 4
  • 12
  • 20

3 Answers3

6

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
ikreb
  • 2,133
  • 1
  • 16
  • 35
6

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] = File() parameter from your endpoint, you would see that a client's JSON request (using a valid JSON payload as request body) would go through without any errors, as the endpoint would expect an application/json-encoded request body (since you have declared each parameter in the endpoint with Body type), and not a multipart/form-data-encoded request body (which would be the case, if UploadFile/File parameter(s) were defined—regardless of whether you defined the rest of parameters as Body fields, they would be expected as form-data instead; in that case, Form type could be used as well, which is a class that inherits directly from Body—see here). You could also confirm that using OpenAPI/Swagger UI autodocs at http://127.0.0.1:8000/docs.

As for declaring a parameter such as policyDetails: List[dict] = Body(...) (or even policyDetails: dict), which is essentially expecting JSON data, you can't do that using Form fields, or Body fields along with File fields (in which case, they will again be interpreted as Form fields, as explained earlier). Hence, the error value is not a valid dict (even though it is not that informative), when attempting to send JSON data (i.e., a dict or list of dict) in a Form field, with the request's Content-Type header actually being set to multipart/form-data (in your case), or even application/x-www-form-urlencoded, if only form-data were included.

Solution

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 in this answer. The files parameter should be defined separately from the model in your endpoint. Below is a working example demonstrating the aforementioned approach.

Working Example

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 = None
    id: str = None
    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: ', [f.filename for f in files])
    return {'input_data':data, 'statusCode': status.HTTP_201_CREATED}

Test the example above

Based on this answer as well.

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 this way:

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/Swagger UI autodocs at http://127.0.0.1:8000/docs.

Chris
  • 18,724
  • 6
  • 46
  • 80
  • In my API, the files can be None or empty list. In your example, I can not send a request without any uploaded file. Do you have any idea? @Chris – Mohammadreza Riahi Jun 22 '23 at 14:44
  • @MohammadrezaRiahi The example above should work as expected - `files` are optional, and hence, one could send a request without having to include any `files` in it. When you declare optional parameters, one shouldn't include such parameters in their request specified with `null` or `None` - please have a look at [this answer](https://stackoverflow.com/a/72289969/17865804) for more details. – Chris Jun 22 '23 at 15:56
  • Thanks. I guess the problem belongs to the fastAPI docs. When I using curl in terminal, everything works fine. – Mohammadreza Riahi Jun 22 '23 at 19:20
1

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

Yagiz Degirmenci
  • 16,595
  • 7
  • 65
  • 85