Python: FastAPI error 422 with POST request when sending JSON data
Asked Answered
L

12

100

I'm building a simple API to test a database. When I use GET request everything works fine, but if I change to POST, I get 422 Unprocessable Entity error.

Here is the FastAPI code:

from fastapi import FastAPI

app = FastAPI()

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

Then, my request using JavaScript

let axios = require('axios')

data = { 
    user: 'smith' 
}

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

Also, using Python requests:

import requests

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

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

I also tried to parse as JSON, enconding using utf-8, and change the headers, but nothing worked for me.

Lavonia answered 27/1, 2020 at 10:15 Comment(0)
C
64

A response having a 422 (unprocessable entity) status code will have a response body that specifies the error message, telling exactly which part of your request is missing or doesn’t match the expected format. The code snippet you povided shows that you are trying to post JSON data to an endpoint that is expecting user being a query parameter, rather than JSON payload. Hence, the 422 unprocessable entity error. Below are given four different options on how to define an endpoint to expect JSON data.

Option 1

As per the documentation, when you need to send JSON data from a client (let's say, a browser) to your API, you send it as a request body (through a POST request). To declare a request body, you can use Pydantic models.

from pydantic import BaseModel

class User(BaseModel):
    user: str

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

Option 2

If one doesn't want to use Pydantic models, they could also use Body parameters. If a single body parameter is used (as in your example), you can use the special Body parameter embed.

from fastapi import Body

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

Option 3

Another (less recommended) way would be to use a Dict type (or simply dict in Python 3.9+) to declare a key:value pair. However, in this way, you can't use custom validations for various attributes in your expected JSON, as you would do with Pydantic models or Body fields (e.g., check if an email address is valid, or if a string follows a specific pattern).

from typing import Dict, Any

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

In the example above, payload could also be defined as payload: dict[Any, Any], or simply payload: dict.

Option 4

If you are confident that the incoming data is a valid JSON, you can use Starlette's Request object directly to get the request body parsed as JSON, using await request.json(). However, with this approach, not only can't you use custom validations for your attributes, but you would also need to define your endpoint with async def, since request.json() is an async method and thus, one needs to await it (have a look at this answer for more details on def vs async def).

from fastapi import Request

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

If you wish, you could also implement some checking on the Content-Type request header value, before attempting to parse the data, similar to this answer. However, just because a request says application/json in the Content-Type header, it doesn't always mean that this is true, or that the incoming data is a valid JSON (i.e., may be missing a curly bracket, have a key that does not have a value, etc). Hence, you could use a try-except block when you attempt to parse the data, letting you handle any JSONDecodeError, in case there is an issue with the way in which your JSON data is formatted.

from fastapi import Request, HTTPException
from json import JSONDecodeError

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

If you would like the endpoint accepting both specific/pre-defined and arbitrary JSON data, please check this answer out.

Test the above options

Using Python requests library

Related answer can be found here.

import requests

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

Using JavaScript Fetch API

Related answers can be found here and here as well. For examples using axios, please have a look at this answer, as well as this answer and this answer.

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);
    });
Carcinoma answered 8/1, 2022 at 20:29 Comment(0)
N
56

Straight from the documentation:

The function parameters will be recognized as follows:

  • If the parameter is also declared in the path, it will be used as a path parameter.
  • If the parameter is of a singular type (like int, float, str, bool, etc) it will be interpreted as a query parameter.
  • If the parameter is declared to be of the type of a Pydantic model, it will be interpreted as a request body."

So to create a POST endpoint that receives a body with a user field you would do something like:

from fastapi import FastAPI
from pydantic import BaseModel


app = FastAPI()


class Data(BaseModel):
    user: str


@app.post("/")
def main(data: Data):
    return data
Narcho answered 28/1, 2020 at 10:58 Comment(0)
L
49

In my case, I was calling the python API from different python project like this

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

I was using the data property, I changed it to json, then it worked for me

queryResponse = requests.post(URL, json = query)
Lusitania answered 15/7, 2021 at 8:16 Comment(3)
Thanks! This got mine working too... but I still wish I knew why passing a dict via data (like the help file instructs) causes the 422 error status code.Colettecoleus
I read that the data field is meant for FormData format... which appears to be a javascript class for passing HTML form data. github.com/tiangolo/fastapi/issues/3373Colettecoleus
Wow 422 unprocessable entity is a data formating issue after all. The error code and message is not explicit.Belgae
B
17

If you're using the fetch API and still getting the 422 Unprocessable Entity, ensure that you have set the Content-Type header:

fetch(someURL, {
  method: "POST",
  headers: {
    "Content-type": "application/json"
  },
  body
}).then(...)

This solved the issue in my case. On the server-side I'm using Pydantic models, so if you aren't using those, see the above answers.

Bedplate answered 16/1, 2022 at 7:36 Comment(1)
For what it's worth, I didn't start having a problem until I updated fastapi from version 0.63 to 0.70. I was banging my head against the wall until I saw your response. I was originally using jquery in which the "type" was set to "json". I changed my save function to use the fetch api with the Content-type set as you have above and problem solved!Haaf
P
5

FastAPI is based on Python type hints so when you pass a query parameter it accepts key:value pair you need to declare it somehow.

Even something like this will work

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

Out: {"user":"Smith"}

But using Pydantic way more effective

class User(BaseModel):
    user: str

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

Out: {"user":"Smith"}
Probabilism answered 26/7, 2020 at 10:16 Comment(0)
I
4

For POST Requests for taking in the request body, you need to do as follows

Create a Pydantic Base Model User

from pydantic import BaseModel

class User(BaseModel):
    user_name: str


@app.post("/")
def main(user: User):
   return user
Imagism answered 16/6, 2020 at 12:28 Comment(0)
R
1

In my case, my FastAPI endpoint is expecting form-data instead of JSON. Thus, the fix is to send form data instead of JSON. (Note: for node-js, FormData is not available and form-data can be used)

Rohn answered 29/12, 2021 at 11:25 Comment(0)
W
1

(if not syntax error like above) There could be many reasons for receiving a response 422 from a post request.

One can replicate this by:

  • editing your body structure
  • changing the type of your body (Send any string)
  • changing/removing your header content type

How I usually debug this is as follows:

  1. If you are using FastAPI, test on the local host using the built-in, '/docs' route, if a post fails there, it's likely a syntax/logic error and not related to your post route. This feature of FastAPI is very helpfully. Note Post requests don't need/expect headers on the UI since it gives you a text place to fill it in.

  2. Test is on a variable body endpoint: you can set this up like:

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

Send a request with any JSON, if you still receive 422, then move to next.

  1. Make sure your header content type is correct, the most common is just:
headers = {'Content-Type': 'application/json'};
Washable answered 26/4, 2022 at 12:47 Comment(0)
A
1

This error seems to be a result of origin hanging up before FastAPI finished.

I was calling FastAPI from Java and it was returning too early.

To fix this error I added usage of CompletableFuture<String> and using HTTPClient.sendAsync function and then calling CompletableFuture.get on the promise.

Adulterate answered 16/2, 2023 at 2:22 Comment(0)
S
0

The issue for me was that my POST's body did not contain all the properties the endpoint was looking for:

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

This is not a very descriptive error and hard to find the cause.

Slain answered 5/8, 2022 at 15:32 Comment(0)
V
0

I encountered - "POST /login HTTP/1.1" 422 Unprocessable Entity error while working on user authentication with FastAPI. This issue was because of how i was capturing auth data from the client. I'll share the solution and what i was doing wrongly

Solution
from fastapi.security import OAuth2PasswordBearer
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import Depends

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

@app.post('/login', response_model=Dict)
def login(
        payload: OAuth2PasswordRequestForm = Depends(),   # <--- here's all that matters
        session: Session = Depends(get_db)
    ):
    ### login logic

In my case, instead of using OAuth2PasswordRequestForm, i was using a UserScheme that capture username and password and declared it a Body param on function signature i.e

@app.post('/login', response_model=Dict)
def login(
        payload: UserSchema = Body()
       ....
       ....
)

The above isn't entirely wrong. It did work very fine when I was using the login endpoint as is.

Build with FastAPI - authorize buttons

On attempt to use the Authorize button for authentication which uses the login endpoint underhood (as that's what's passed as tokenUrl) that's when i get the Unprocessable Entity Error

Hope this helps

Edit 1 ( adding some more context )

Vanmeter answered 2/2, 2023 at 21:26 Comment(0)
H
0

The short answer: the error indicates that the wrong content type is passed.

I started getting these errors from curl when I upgraded FastAPI from 0.61.2 to 0.101.1. My command was like this:

curl -i -X POST http://localhost:8000/model -d '{"text":"o"}'

It turned out that the new FastAPI requires to specify the content type:

curl -i -X POST http://localhost:8000/model -H "Content-type: application/json" -d '{"text":"o"}'
Heeley answered 25/8, 2023 at 11:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.