【问题标题】:"422 Unprocessable Entity" error when making POST request with both attributes and key using FastAPI使用 FastAPI 发出带有属性和键的 POST 请求时出现“422 Unprocessable Entity”错误
【发布时间】:2021-12-18 16:04:03
【问题描述】:

我有一个名为main.py 的文件如下:

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

现在,如果我运行测试代码,保存在文件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())

我没有得到想要的结果,即

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

错在哪里?

【问题讨论】:

  • 我不相信您可以直接从视图返回模型对象。您必须将其包装在 JSONResponse 中。
  • 但是response.json() == {"id": "baz", "title": "A test title", "description": "A test description"} 也不起作用。我正在尝试在函数test_create_item() 中执行此操作:fastapi.tiangolo.com/tutorial/testing
  • 该示例显式返回一个字典,而您没有这样做。
  • 通过GET 请求,我可以毫无问题地返回response.json()。但是,在fake_db 中插入对象也不起作用。
  • 您为什么希望key 参数从您的请求中获得任何值?您没有提供任何密钥 - 您的意思是从 Form 值分配它吗?您可能应该从提交项目的id 中提取密钥,或者创建一个复合请求模型(即在根级别同时具有项目和密钥的 CreateItem 请求)。

标签: python fastapi


【解决方案1】:

让我们先解释一下你做错了什么。


FastAPI's TestClient 只是Starlette's TestClient 的再导出,requests.Session 的子类。 requests 库的 post 方法具有以下签名:

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
    """

你的代码

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

做错了几件事:

  1. 它将参数传递给 both data json 参数,这是错误的,因为您不能有 2 个不同的请求主体。您要么传入data json但不能同时传入data 通常用于来自 HTML 表单的表单编码输入,而 json 用于原始 JSON 对象。请参阅requests docs on "More complicated POST requests"
  2. 请求库将简单地删除 json 参数,因为:

    注意,如果datafiles 被传递,json 参数将被忽略。

  3. 它将纯字符串"Baz" 传递给json 参数,这不是有效的JSON 对象。
  4. data 参数需要表单编码的数据。

FastAPI 在响应中返回的完整错误是:

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'}]}

第一个错误表示查询中缺少 key,这意味着路由参数 key"Baz" 不在请求正文中,FastAPI 尝试从查询参数中查找它(请参阅 FastAPI 文档Request body + path + query parameters)。

第二个错误来自我上面列出的关于 data 未正确形式编码的第 4 点(当您将 dict 值包装在 json.dumps 中时,该错误确实消失了,但这并不重要,也不是它的一部分解决方案)。

你说in a comment 你正试图做与FastAPI Testing Tutorial 相同的事情。该教程与您的代码的不同之处在于将 Item 对象的所有属性发布在 1 个正文中,并且它使用了 .postjson= 参数。


现在是解决方案!

解决方案 #1:有一个单独的类用于使用键发布项目属性

在这里,您需要 2 个类,一个具有 key 属性,用于 POST 请求正文(我们称之为 NewItem),而您当前的一个 Item 用于内部数据库和响应模型。然后,您的路由函数将只有 1 个参数 (new_item),您可以从该对象中获取 key

ma​​in.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

对于测试.post 代码,使用json= 传递1 个字典中的所有字段。

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()

输出

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

解决方案 #2:有 2 个身体部位,1 个用于项目属性,1 个用于键

您可以改为这样构建 POSTed 正文:

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

您在嵌套字典中拥有Item 属性,然后在与item 相同的级别中拥有一个简单的key-值对。 FastAPI 可以处理这个问题,请参阅Singular values in body 上的文档,它非常适合您的示例:

例如,扩展之前的模型,您可以决定除了itemuser 之外,还希望在同一主体中拥有另一个密钥importance

如果你按原样声明它,因为它是一个奇异值,FastAPI 会假定它是一个query 参数。

但您可以指示 FastAPI 将其视为另一个主体 key 使用 Body

注意我强调的部分,关于告诉 FastAPI 在同一个正文中查找key。这里重要的是参数名称itemkey 与请求正文中的名称匹配。

ma​​in.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

同样,为了发出.post 请求,请使用json= 传递整个字典。

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()

输出

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

【讨论】:

  • 首先感谢您的回答。非常清晰和详细,我非常感谢它。我有两个问题。 1) 在解决方案#2 > main.py > create_item() 中,如果除了itemkey 之外还有另一个参数,表单会是什么? create_item(item: Item, key: str = Body(...), another_parameter: int = Body(...)) 正确吗? 2) 在解决方案#2 > test_main.py > test_create_item() 中,我必须通过json={...} 传递数据是否正确?我不能传递main.py 中定义的类的对象吗(例如Item,为简单起见,我们假设key 不存在)?
  • @LJG 对于 1),是的,这是正确的。请参阅有关多个主体参数的 FastAPI 教程:fastapi.tiangolo.com/tutorial/body-multiple-params/…
  • @LJG 对于 2),类的对象需要格式化为表单编码数据(对于 data=)或 JSON 对象(对于 json=)。发出 POST 请求时没有传递“类的对象”这样的事情。我建议阅读有关发出 POST 请求的请求文档:docs.python-requests.org/en/latest/user/quickstart/…。 Pydantic 提供了一种将BaseModel 导出为普通字典(pydantic-docs.helpmanual.io/usage/exporting_models)的方法,您可以直接将其传递给json=。使用json= 是最简单的方法。
  • 对于 1),我并不完全清楚在一般情况下如何使用 Body(...)。例如 create_item(item: Item, key: str = Body(...), another_parameter: int = Body(...)) 有效,但 create_item(key: str = Body(...), item : Item, another_parameter: int = Body(...)) 无效。比如我要定义一个函数fun(item1: List[Item1], item2: Item2, id: int, key: str),哪些参数应该标记为Body(...)Item1Item2是两个泛型类)?目标是通过json={"item1": [{...}, {...}], "item2": {...}, "id": 1, "key": "abc"}
猜你喜欢
  • 2023-01-07
  • 2021-12-19
  • 2022-07-19
  • 2022-11-08
  • 2020-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-06
相关资源
最近更新 更多