Python:使用POST请求发送JSON数据时,FastAPI出现422错误

90
我正在构建一个简单的API来测试数据库。当我使用GET请求时一切正常,但如果我改为POST,我会收到422 Unprocessable Entity错误。
这是FastAPI代码:
from fastapi import FastAPI

app = FastAPI()

@app.post("/")
def main(user):
    return user

然后,我的请求使用JavaScript

let axios = require('axios')

data = { 
    user: 'smith' 
}

axios.post('http://localhost:8000', data)
    .then(response => (console.log(response.url)))

此外,使用Python的requests库:
import requests

url = 'http://127.0.0.1:8000'
data = {'user': 'Smith'}

response = requests.post(url, json=data)
print(response.text)

我也尝试过使用utf-8编码解析为JSON,并更改头文件,但对我来说都没有起作用。


你是否启动了 fastapi 应用程序运行的服务器(如 uvicorn)? - Orkun Kocyigit
12个回答

55

直接引用文档中的原话:

函数参数将被识别为如下:

  • 如果该参数也在路径中声明,它将被用作路径参数。
  • 如果该参数是单一类型(例如int、float、str、bool等),它将被解释为查询参数。
  • 如果该参数声明为Pydantic模型的类型,则它将被解释为请求正文

因此,要创建一个POST端点,接收带有用户字段的正文,您可以这样做:

from fastapi import FastAPI
from pydantic import BaseModel


app = FastAPI()


class Data(BaseModel):
    user: str


@app.post("/")
def main(data: Data):
    return data

51
一个状态码为422不可处理的实体)的响应将包含一个响应体,其中指定了错误消息,告诉您的请求中缺少或不符合预期格式的部分。您提供的代码片段显示您正在尝试将JSON数据发布到一个期望user作为query参数而不是JSON负载的端点。因此,出现了422不可处理的实体错误。下面给出了四种不同的选项,用于定义一个期望JSON数据的端点。

选项1

根据文档,当您需要从客户端(比如浏览器)向API发送JSON数据时,您将其作为请求体(通过POST请求)发送。要声明请求体,您可以使用Pydantic模型。
from pydantic import BaseModel

class User(BaseModel):
    user: str

@app.post('/')
def main(user: User):
    return user

选项2

如果不想使用Pydantic模型,也可以使用Body参数。如果只使用一个body参数(例如您的示例),可以使用特殊的Body参数embed

from fastapi import Body

@app.post('/')
def main(user: str = Body(..., embed=True)):
    return {'user': user}

选项3

另一种(不太推荐的)方法是使用Dict类型(或在Python 3.9+中简单地使用dict)来声明key:value对。然而,通过这种方式,您无法像Pydantic模型或Body字段那样对预期的JSON中的各个属性使用自定义验证(例如,检查电子邮件地址是否有效,或者字符串是否遵循特定的模式)。

from typing import Dict, Any

@app.post('/')
def main(payload: Dict[Any, Any]):  # or, payload: dict[Any, Any]
    return payload

选项4

如果您确信传入的数据是有效的JSON,您可以直接使用Starlette的Request对象来解析请求体作为JSON,使用await request.json()。然而,使用这种方法不仅无法对属性进行自定义验证,还需要使用async def来定义您的端点,因为request.json()是一个async方法,因此需要使用await(请参阅此答案以获取有关defasync def的更多详细信息)。

from fastapi import Request

@app.post('/')
async def main(request: Request): 
    return await request.json()

如果你希望的话,在尝试解析数据之前,你也可以对请求头中的Content-Type值进行一些检查,类似于this answer。然而,仅仅因为一个请求在Content-Type头中声明为application/json并不总是意味着这是真实的,或者传入的数据是有效的JSON(例如,可能缺少花括号、键没有对应的值等)。因此,当你尝试解析数据时,你可以使用try-except块来处理任何JSONDecodeError,以防你的JSON数据格式有问题。
from fastapi import Request

@app.post('/')
async def main(request: Request):
    content_type = request.headers.get('Content-Type')
    
    if content_type is None:
        return 'No Content-Type provided.'
    elif content_type == 'application/json':
        try:
            json = await request.json()
            return json
        except JSONDecodeError:
            return 'Invalid JSON data.'
    else:
        return 'Content-Type not supported.'

测试上述选项

使用Python的requests库

相关答案可以在这里找到。

import requests

url = 'http://127.0.0.1:8000/'
payload ={'user': 'foo'}
resp = requests.post(url=url, json=payload)
print(resp.json())

使用JavaScript Fetch API

相关答案也可以在这里这里找到。关于使用axios的示例,请参考此答案,以及此答案此答案

fetch('/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({'user': 'foo'})
    })
    .then(resp => resp.json()) // or, resp.text(), etc
    .then(data => {
        console.log(data); // handle response data
    })
    .catch(error => {
        console.error(error);
    });

1
Chris,你关于“Payload”的建议对我很有帮助。非常感谢。 - Iker

49

在我的情况下,我像这样从不同的Python项目中调用Python API

queryResponse = requests.post(URL, data= query)

我以前使用的是data属性,现在改为json格式,这样就可以了

queryResponse = requests.post(URL, json = query)

1
谢谢!这也让我的程序运行起来了...但我仍然希望知道为什么通过data传递字典(就像帮助文件所指示的那样)会导致422错误状态码。 - Mark Seagoe
1
我读到数据字段是为FormData格式而设计的...这似乎是一个用于传递HTML表单数据的JavaScript类。https://github.com/tiangolo/fastapi/issues/3373 - Mark Seagoe
1
哇,422不可处理实体错误毕竟是数据格式问题。 错误代码和消息并不明确。 - Alsushi

17
如果您正在使用 fetch API,但仍然收到422 Unprocessable Entity错误,请确保已设置Content-Type头部:
fetch(someURL, {
  method: "POST",
  headers: {
    "Content-type": "application/json"
  },
  body
}).then(...)

这在我的情况下解决了问题。在服务器端,我正在使用Pydantic模型,所以如果您没有使用这些模型,请参阅上面的答案。



1
说实话,我在将fastapi从版本0.63更新到0.70之后才开始遇到问题。在看到你的回复之前,我一直在苦苦思索。我最初使用的是jquery,其中“type”设置为“json”。我将我的保存函数更改为使用fetch api,并将Content-type设置为您上面提到的内容,问题得到了解决! - David W.

5

FastAPI基于Python类型提示,因此当您传递查询参数时,它接受:对,您需要以某种方式声明它。

即使像这样也可以工作

from typing import Dict, Any
...
@app.post("/")
def main(user: Dict[Any, Any] = None):
    return user

Out: {"user":"Smith"}

但是使用Pydantic更加有效

class User(BaseModel):
    user: str

@app.post("/")
def main(user: User):
    return user

Out: {"user":"Smith"}

4

对于需要获取请求体的POST请求,您需要按照以下步骤进行操作:

创建一个Pydantic基本模型User

from pydantic import BaseModel

class User(BaseModel):
    user_name: str


@app.post("/")
def main(user: User):
   return user

1

如果没有像上面那样的语法错误,从post请求收到响应422可能有很多原因。

可以通过以下方式复制此操作:

  • 编辑您的正文结构
  • 更改正文类型(发送任何字符串)
  • 更改/删除标题内容类型

我通常是这样调试的:

  1. 如果您正在使用FastAPI,请使用内置的'/docs'路由在本地主机上进行测试,如果发生故障,则很可能是语法/逻辑错误,而不是与您的发布路由相关。 FastAPI的这个功能非常有用。请注意,在UI上,Post请求不需要/期望标题,因为它提供了一个填写文本的地方。

  2. 在变量正文端点上进行测试:您可以设置如下:

@app.post('/test')
async def function(objectName: dict = Body(...)):

发送任何JSON请求,如果仍然收到422错误,则继续下一步。

  1. 确保您的标题内容类型正确,最常见的只是:
headers = {'Content-Type': 'application/json'};

1

这个错误似乎是由于FastAPI还没有完成操作,而源已经挂起导致的。

我正在从Java中调用FastAPI,但返回结果过早。

为了解决这个错误,我添加了使用CompletableFuture<String>并使用HTTPClient.sendAsync函数,然后在承诺上调用CompletableFuture.get


1

在我的情况下,我的FastAPI端点期望的是表单数据而不是JSON。因此,修复方法是发送表单数据而不是JSON。(注意:对于node-js,FormData不可用,可以使用form-data


0
对我来说问题在于我的POST请求体中没有包含终端点所需的所有属性:
POST
{
    "gateway": "",
    "nameservers": [],
    "deleteWiFi": true,
    "ssidPassword": ""
}

FastAPI python

class SubmitWiFi(BaseModel):
    gateway: str
    addresses: list[str]    # missing
    nameservers: list[str]
    deleteWiFi: bool
    ssid: str   # missing
    ssidPassword: str

@app.post("/submitWiFi")
async def submitWiFi(data: SubmitWiFi):
    # my code

这并不是一个非常具描述性的错误,很难找到原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接