A common challenge when building a FastAPI application is handling different content types, like JSON
and Form
data. Any many cases your API needs to support both formats, especially when dealing with web forms and modern front-end applications that often send json payloads.
This post demonstrates how to build a FastAPI application that handles and validates both JSON and from data inputs on a single endpoint.
The Challenge: Handling Different Content Types
By default, web browsers use application/x-www-form-urlencoded
or multipart/form-data
content types when submitting forms. On the other hand, APIs and modern front-ends typically send application/json
. Our goal is to handle both efficiently and return appropriate responses.
To achieve this I’ll:
- Use dependency injection in FastAPI to dynamically process the request type (form or json).
- Validate the incoming data using Pydantic.
- Handle errors gracefully, including invalid content types and validation errors.
Code Breakdown
Let’s dive into each part of this code to understand how it works.
Validation
We define a Pydantic model User
to enforce structure and validation on the incoming data. In this case, it expects a single field: name
. This model will automatically validate the incoming data whether it comes from a form or a json body.
You can extend this model by adding more fields as needed.
from pydantic import BaseModel
class User(BaseModel):
name: str
Request Type
Below, I use the request_type
function to dynamically handle both JSON
and Form
content types. The Request
object is used to inspect the content type of the incoming request:
- If the content type is
application/x-www-form-urlencoded
, we treat it as form data and convert it to a dictionary usingawait request.form()
. - If it’s
application/json
, we parse it into a dictionary usingawait request.json()
. - If the content type is unsupported, we raise a
ValueError
.
Later on the request_type
function is injected into the route via FastAPI’s Depends
feature.
from fastapi import Request
async def request_type(request: Request) -> dict:
# Get content-type (json or form) from request
content_type = request.headers.get("Content-Type")
# Convert form to dict
if content_type == "application/x-www-form-urlencoded":
form_data = await request.form()
data = dict(form_data)
# Convert json to dict
elif content_type == "application/json":
data = await request.json()
# Handle unsupported content types
else:
raise ValueError(f"Unsupported content type: {content_type}")
return data
Handle Request in Route
This is the main route for handling incoming requests. The data
returned by the request_type
function is validated against the User
model. If the validation succeeds, it means that the request body (whether JSON or form) is valid and contains the correct structure.
When the request’s content type is form data (application/x-www-form-urlencoded
), we return a simple HTML response. This can of course be extended to return more complex HTML content based on your needs.
For JSON requests, the validated data is returned as the response.
import json
from fastapi import Depends, FastAPI
from fastapi.responses import JSONResponse, HTMLResponse
app = FastAPI()
@app.post("/")
async def root(request: Request, data: dict = Depends(request_type)):
try:
# Validate data using pydantic model
user = User(**data)
# content-type is form, return HTML response
if request.headers.get("Content-Type") == "application/x-www-form-urlencoded":
return HTMLResponse(f"<p>Hello, {user.name}!</p>")
# content-type not form return dict/json
return user
except ValidationError as e:
# Exception handling when form or json data is invalid
return JSONResponse(
content=json.loads(e.json()),
status_code=400,
)
except ValueError as e:
# Handle unsupported content type
return JSONResponse(
content={"error": str(e)},
status_code=415, # Unsupported Media Type
)
If the data doesn’t meet the Pydantic model’s validation rules, a ValidationError
is raised, and a 400 Bad Request
error with the detailed validation errors is returned as a JSON response.
When the content type is unsupported, we catch the ValueError
and return a 415 Unsupported Media Type
error with a descriptive message.
Testing
We can now make a request using a json body or a form to the same endpoint
curl --location 'http://127.0.0.1:8000 ' \
--header 'Content-Type: application/json' \
--data '{
"name": "John Doe"
}'
This will return the following json response.
{
"name": "John Doe"
}
We can also make a request using a form
curl --location 'http://127.0.0.1:8000 ' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'name=John Doe'
This results in a HTML string
<p>Hello, John Doe!</p>
When incorrect fields are specified this will result in a nice pydantic validation error response.
curl --location 'http://127.0.0.1:8000 ' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'user=John Doe'
The incorrect field user
is used this will result in the following response
[
{
"type": "missing",
"loc": [
"name"
],
"msg": "Field required",
"input": {
"user": "John Doe"
},
"url": "https://errors.pydantic.dev/2.6/v/missing"
}
]
Benefits
This pattern allows for seamlessly processing both form and JSON data, making our API more flexible.
It also leverages Pydantic’s integration with FastAPI ensures that incoming json or form data is validated automatically based on the schema we define.
Unsupported content types and validation errors are handled gracefully, providing clear error messages to the client.
By using dependency injection (Depends
), the code remains modular. The logic for handling and converting request data is abstracted away, making the route handler simpler and more readable.
Links
[[rest]] [[api]] [[python]] [[fastapi]]