POST request to FastAPI endpoint produces error 422: "value is not a valid dict"

David Y.
jump to solution

The Problem

I have a Python script that sends a POST request to an API I’ve built using FastAPI. The POST request fails with error code 422 and the following details:

{
  "detail": [
    {
      "loc": [ "body" ],
      "msg": "value is not a valid dict",
      "type": "type_error.dict"
    }
  ]
}

My client script looks like this:

import requests, pandas, json

dataframe = pandas.DataFrame({
    "Firstname": ["Joaquin"],
    "Surname": ["Phoenix"],
    "Age": [49]
})

df_json = dataframe.to_json(orient="records")
response = requests.post('http://localhost:8000/create-user/',
                           json=json.loads(df_json),
                           headers={"Content-Type": "application/json"})
print(response.json())

The relevant API endpoint and Pydantic model look like this:

from pydantic import BaseModel
from fastapi import FastAPI

app = FastAPI()

class User(BaseModel):
    firstname: str
    surname: str
    age: int

@app.post("/create-user/")
async def create_users(user: User):
    # ... user creation operations ...
    return {"Message": "User created."}

What’s going wrong? Is it a problem with my client script or my API code?

The Solution

The problem is in the client script. The requests.post function expects a dictionary to be passed to the json keyword argument, which it will automatically convert to JSON.

This script first converts a Pandas DataFrame to a JSON string (dataframe.to_json(orient="records")), which looks like this:

[{"Firstname":"Joaquin", "Surname": "Phoenix","Age": 49}]

The script then uses json.loads to convert it back into a Python object. Because of the argument orient="records" in the original conversion, the object created here will be a list rather than a dictionary, leading to the 422 error when it is submitted.

We can fix this problem by converting the DataFrame to a dictionary instead of a JSON string:

import requests, pandas

dataframe = pandas.DataFrame({
    "Firstname": ["Joaquin"],
    "Surname": ["Phoenix"],
    "Age": [49]
})

userdata = dataframe.to_dict(orient="records")
# will be a list: [{"Firstname":"Joaquin", "Surname": "Phoenix","Age": 49}]
userdata = userdata[0] # take the first item from the list
# will be a dictionary: {"Firstname":"Joaquin", "Surname": "Phoenix","Age": 49}
prediction = requests.post('http://localhost:8000/create-users/',
                           json=userdata,
                           headers={"Content-Type": "application/json"})

This altered code should produce the expected results.

Considered "not bad" by 4 million developers and more than 150,000 organizations worldwide, Sentry provides code-level observability to many of the world's best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.

Sentry