Custom Validation Errors in FastAPI with Pydantic
Abenezer Belachew Β· January 01, 2024
5 min read
When working with FastAPI and Pydantic, you may occasionally find that the standard validation error messages don't quite align with your requirements. To better illustrate this point, let's consider a straightforward example involving a route designed for updating a post:
main.py
@app.put("/{post_id}", response_model=schemas.PostResponse)
def update_post(
post_id: int,
post: schemas.PostUpdate,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(oauth2.get_current_user),
):
db_post = get_post_or_404(db, post_id)
# Check if the user is the owner of the post
if is_post_owner(db_post, current_user):
# code for updating
- In this setup, the route receives a
post_id
as a URL parameter and uses it to retrieve the corresponding post from the database. - The update process is initiated only if the post exists and the logged-in user is its author. However, the focus here extends beyond just the
post_id
. - The route also anticipates receiving a
post
object. This object conforms to aPostUpdate
schema, which we've defined separately in another file. You can disregard the details concerningdb
andcurrent_user
for this discussion. They're not crucial to the aspect we're focusing on. - Say the
PostUpdate
schema looks something like this:
schemas.py
class PostUpdate(BaseModel):
title: str
content: str
published: bool = False
rating: Optional[int] = None
- The core expectation here is to receive at least a
title
andcontent
. For the fieldspublished
andrating
, the system is designed to use default values if they are not explicitly provided, ensuring flexibility and robustness in data handling. - So, what if we hit the endpoint and send a title that isnβt a string? What would happen?
curl -i --request PUT \
--url http://localhost:8000/posts/38 \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiaGkgdGhlcmUsIGN1cmlvdXMgZnJpZW5kIiwiZXhwIjoxNzA1ODUyMDM3fQ.qpNy5poAYReVIcacP8GxRKW8DoMlVqs6ljoIq9QNYOw' \
--header 'Content-Type: application/json' \
--data '{
"title": 25,
"content": "this is the updated content"
}'
HTTP/1.1 422 Unprocessable Entity
date: Mon, 01 Jan 2024 08:42:27 GMT
server: uvicorn
content-length: 162
content-type: application/json
{
"detail": [
{
"type": "string_type",
"loc": [
"body",
"title"
],
"msg": "Input should be a valid string",
"input": 25,
"url": "https://errors.pydantic.dev/2.5/v/string_type"
}
]
}
- As you can see, we got a response detail that looks something like this:
{
"detail": [
{
"type": "string_type",
"loc": [
"body",
"title"
],
"msg": "Input should be a valid string",
"input": 25,
"url": "https://errors.pydantic.dev/2.5/v/string_type"
}
]
}
- It's good and informative, but that's not quite what we want. Let's add some customization to the validation error response.
{
"status": "validation_error",
"detail": "Input validation failed π",
"errors": [
{
"field": "body -> title",
"message": "Input should be a valid string π¨",
"error_type": "string_type",
"suggestion": "Check this field for errors. π οΈ"
}
],
"tip": "Please review the errors and try again! π",
"custom_docs": "http://localhost:8000/docs/posts/38",
"custom_support": "555-I-WONT-PICK-UP"
}
- To accomplish this, we can customize the
RequestValidationError
from FastAPI. This modification can be implemented in the section of your code where your app is defined. Here's how you can do it:
main.py
from fastapi.exceptions import RequestValidationError
from fastapi.requests import Request
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = exc.errors()
formatted_errors = []
def generate_helpful_message(error):
# Custom logic to generate a helpful message based on error type/location
if error["type"] == "type_error.integer":
return "Ensure this value is an integer. π’"
elif error["type"] == "value_error.missing":
return "This field is required and cannot be missing. β"
# Add more cases as needed
return "Check this field for errors. π οΈ"
for error in errors:
field = " -> ".join(map(str, error["loc"]))
message = error["msg"]
helpful_message = generate_helpful_message(error)
pretty_message = {
"field": field,
"message": f"{message} π¨",
"error_type": error["type"],
"suggestion": helpful_message,
}
formatted_errors.append(pretty_message)
# Generate a link to the docs for this endpoint
docs_url = (
f"{request.base_url.scheme}://{request.base_url.netloc}/docs{request.url.path}"
)
return JSONResponse(
status_code=422,
content={
"status": "validation_error",
"detail": "Input validation failed π",
"errors": formatted_errors,
"tip": "Please review the errors and try again! π",
"custom_docs": f"{docs_url}", # Link to your API documentation
"custom_support": "555-I-WONT-PICK-UP", # Link to your support page
},
)
Letβs try it now.
curl -i --request PUT \
--url http://localhost:8000/posts/38 \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo0LCJleHAiOjE3MDU4NTIwMzd9.TI6j0t8wBOrAz_00NDI-FdtSjtMqtlv3w-Gl2ZUwz9c' \
--header 'Content-Type: application/json' \
--header 'User-Agent: insomnia/8.4.5' \
--data '{
"title": 25,
"content": "this is the content"
}'
HTTP/1.1 422 Unprocessable Entity
date: Mon, 01 Jan 2024 09:20:40 GMT
server: uvicorn
content-length: 376
content-type: application/json
{
"status": "validation_error",
"detail": "Input validation failed π",
"errors": [
{
"field": "body -> title",
"message": "Input should be a valid string π¨",
"error_type": "string_type",
"suggestion": "Check this field for errors. π οΈ"
}
],
"tip": "Please review the errors and try again! π",
"custom_docs": "http://localhost:8000/docs/posts/38",
"custom_support": "555-I-WONT-PICK-UP"
}
- Notice how the new validation error is much more informative? While using emojis in responses might not be conventional or recommended due to compatibility and parsing issues, they certainly add a visually appealing element. Plus, if you have custom documentation, you can link to it directly, offering users a way to troubleshoot issues. And as a last resort, you can even add a custom support line in the response for additional help.