Skip to content
Advertisement

“422 Unprocessable Entity” error when making POST request with both attributes and key using FastAPI

I have a file called main.py as follows:

from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

fake_db = {
    "Foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
    "Bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}

class Item(BaseModel):
    id: str
    title: str
    description: Optional[str] = None

@app.post("/items/", response_model=Item)
async def create_item(item: Item, key: str):
    fake_db[key] = item
    return item

Now, if I run the code for the test, saved in the file test_main.py

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_item():
    response = client.post(
        "/items/",
        {"id": "baz", "title": "A test title", "description": "A test description"},
        "Baz"
    )
    return response.json()

print(test_create_item())

I don’t get the desired result, that is

{"id": "baz", "title": "A test title", "description": "A test description"}

What is the mistake?

Advertisement

Answer

Let’s start by explaining what you are doing wrong.


FastAPI’s TestClient is just a re-export of Starlette’s TestClient which is a subclass of requests.Session. The requests library’s post method has the following signature:

def post(self, url, data=None, json=None, **kwargs):
    r"""Sends a POST request. Returns :class:`Response` object.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) json to send in the body of the :class:`Request`.
    :param **kwargs: Optional arguments that ``request`` takes.
    :rtype: requests.Response
    """

Your code

response = client.post(
    "/items/",
    {"id": "baz", "title": "A test title", "description": "A test description"},
    "Baz",
)

is doing multiple things wrong:

  1. It is passing in arguments to both the data and the json parameter, which is wrong because you can’t have 2 different request bodies. You either pass in data or json, but not both. The data is typically used for form-encoded inputs from HTML forms, while json is for raw JSON objects. See the requests docs on “More complicated POST requests”.
  2. The requests library will simply drop the json argument because:

    Note, the json parameter is ignored if either data or files is passed.

  3. It is passing-in the plain string "Baz" to the json parameter, which is not a valid JSON object.
  4. The data parameter expects form-encoded data.

The full error returned by FastAPI in the response is:

def test_create_item():
    response = client.post(
        "/items/", {"id": "baz", "title": "A test title", "description": "A test description"}, "Baz"
    )
    print(response.status_code)
    print(response.reason)
    return response.json()

# 422 Unprocessable Entity
# {'detail': [{'loc': ['query', 'key'],
#              'msg': 'field required',
#             'type': 'value_error.missing'},
#             {'loc': ['body'],
#              'msg': 'value is not a valid dict',
#             'type': 'type_error.dict'}]}

The 1st error says key is missing from the query, meaning the route parameter key value "Baz" was not in the request body and FastAPI tried to look for it from the query parameters (see the FastAPI docs on Request body + path + query parameters).

The 2nd error is from point #4 I listed above about data not being properly form-encoded (that error does go away when you wrap the dict value in json.dumps, but that’s not important nor is it part of the solution).

You said in a comment that you were trying to do the same thing as in the FastAPI Testing Tutorial. The difference of that tutorial from your code is that was POSTing all the attributes of the Item object in 1 body, and that it was using the json= parameter of .post.


Now on the solutions!

Solution #1: Have a separate class for POSTing the item attributes with a key

Here, you’ll need 2 classes, one with a key attribute that you use for the POST request body (let’s call it NewItem), and your current one Item for the internal DB and for the response model. Your route function will then have just 1 parameter (new_item) and you can just get the key from that object.

main.py

class Item(BaseModel):
    id: str
    title: str
    description: Optional[str] = None

class NewItem(Item):
    key: str

@app.post("/items/", response_model=Item)
async def create_item(new_item: NewItem):
    # See Pydantic https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict
    # Also, Pydantic by default will ignore the extra attribute `key` when creating `Item`
    item = Item(**new_item.dict())
    print(item)
    fake_db[new_item.key] = item
    return item

For the test .post code, use json= to pass all the fields in 1 dictionary.

test_main.py

def test_create_item():
    response = client.post(
        "/items/",
        json={
            "key": "Baz",
            "id": "baz",
            "title": "A test title",
            "description": "A test description",
        },
    )
    print(response.status_code, response.reason)
    return response.json()

Output

id='baz' title='A test title' description='A test description'
200 OK
{'description': 'A test description', 'id': 'baz', 'title': 'A test title'}

Solution #2: Have 2 body parts, 1 for the item attributes and 1 for the key

You can structure the POSTed body like this instead:

{
    "item": {
        "id": "baz",
        "title": "A test title",
        "description": "A test description",
    },
    "key": "Baz",
},

where you have the Item attributes in a nested dict and then have a simple key-value pair in the same level as item. FastAPI can handle this, see the docs on Singular values in body, which fits your example quite nicely:

For example, extending the previous model, you could decide that you want to have another key importance in the same body, besides the item and user.

If you declare it as is, because it is a singular value, FastAPI will assume that it is a query parameter.

But you can instruct FastAPI to treat it as another body key using Body

Note the parts I emphasized, about telling FastAPI to look for key in the same body. It is important here that the parameter names item and key match the ones in the request body.

main.py

from fastapi import Body, FastAPI

class Item(BaseModel):
    id: str
    title: str
    description: Optional[str] = None

@app.post("/items/", response_model=Item)
async def create_item(item: Item, key: str = Body(...)):
    print(item)
    fake_db[key] = item
    return item

Again, for making the .post request, use json= to pass the entire dictionary.

test_main.py

def test_create_item():
    response = client.post(
        "/items/",
        json={
            "item": {
                "id": "baz",
                "title": "A test title",
                "description": "A test description",
            },
            "key": "Baz",
        },
    )
    print(response.status_code, response.reason)
    return response.json()

Output

id='baz' title='A test title' description='A test description'
200 OK
{'description': 'A test description', 'id': 'baz', 'title': 'A test title'}
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement