๐งฑ Enhancing Field Validation in BookRequest
In the previous step, we introduced field-level validation inside the BookRequest model using Pydantic.
Now, weโre going to deepen that understanding and improve how our API communicates expectations to the client.
๐ Remember:
Validation is not just about preventing bad data.
Itโs about clearly defining backend rules.
๐ Understanding the Current Validation Rules
Here is the existing model:
class BookRequest(BaseModel): # ๐ pydantic model
id: Optional[int] = None
title: str = Field(min_length=3)
author: str = Field(min_length=1)
description: str = Field(min_length=1, max_length=100)
rating: int = Field(gt=0, lt=6)Letโs break this down properly ๐
idfield is optional, and its default value is null (None).titlemust have at least 3 charactersauthormust not be emptydescriptionmust be between 1 and 100 charactersratingmust be:Greater than 0
Less than 6
๐ง Backend Fundamental Insight
When a request hits your FastAPI endpoint:
FastAPI reads the request body.
It passes the data into the
BookRequestmodel.Pydantic validates every field.
If validation fails โ FastAPI automatically returns a 422 Unprocessable Entity response.
This happens before your endpoint function executes.
That is extremely important.
๐ Your route logic only runs if the data passes validation.
This is how FastAPI protects your backend automatically.
๐ Improving the ID Field Description
Currently:
id: Optional[int] = NoneThis works โ but it lacks clarity in the API documentation.
We can improve it like this:
id: Optional[int] = Field(description="ID is not required", default=None)๐ฏ Why This Matters
FastAPI automatically generates API documentation using:
๐ง OpenAPI
๐ Swagger UI
By adding a description:
๐ We are improving the API contract clarity.
Now when users view the schema in Swagger UI, they understand:
The
idexistsBut it is not required when creating a book
This improves developer experience significantly.
๐งญ Guiding Users with Model Configuration
We can take things further by adding a request example.
Here is the improved model:
class BookRequest(BaseModel):
id: Optional[int] = Field(description="ID is not required", default=None)
title: str = Field(min_length=3)
author: str = Field(min_length=1)
description: str = Field(min_length=1, max_length=100)
rating: int = Field(gt=0, lt=6)
model_config = {
"json_schema_extra": {
"example": {
"title": "A new book",
"author": "Name of the author",
"description": "The description of the book",
"rating": 5
}
}
}๐ What Does model_config Do?
model_config is used to customize how the model is displayed in:
OpenAPI schema
Swagger UI documentation
In this case, it adds a custom example request body to the generated docs.
๐ It does NOT affect data validation
๐ It does NOT change backend behavior
๐ It is only for improving documentation
This highlights an important backend principle:
Validation and documentation are separate concerns.
You are defining both:
How the API enforces rules (validation)
How easy the API is to understand and use (documentation)
This is a sign of well-designed, professional APIs.
๐ Viewing the Example in the Browser
๐ฅ Open your browser
๐ Navigate to: http://127.0.0.1:8000/docs
Under the Create_book (POST) endpoint:
You will notice:
โ The
idfield appears in the schemaโ The
idfield is NOT included in the example (because not included in json_schema_extra which act as placeholder request body)
๐ค Why?
Because:
The
idis auto-generated.The client does not provide it when creating a new book.
This clearly communicates:
What the backend expects
What the backend controls
๐ง Backend Concept Reinforcement
Letโs pause and understand something important.
When creating a resource:
The client sends data.
The backend validates it.
The backend assigns an ID.
The backend stores it.
This follows a core backend principle:
The server owns resource identity.
Thatโs why id is optional in requests โ but required internally.
๐๏ธ books2.py
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Book:
id: int
title: str
author: str
description: str
rating: int
def __init__(self, id, title, author, description, rating):
self.id = id
self.title = title
self.author = author
self.description = description
self.rating = rating
class BookRequest(BaseModel):
id: Optional[int] = Field(description="ID is not required", default=None) <---
title: str = Field(min_length=3)
author: str = Field(min_length=1)
description: str = Field(min_length=1, max_length=100)
rating: int = Field(gt=0, lt=6)
model_config = {
"json_schema_extra": {
"example": {
"title": "A new book",
"author": "Name of the author",
"description": "The description of the book",
"rating": 5
}
}
}
BOOKS = [
Book(1, 'Computer Science Pro', 'codingwithrobby', 'A very nice book', 3),
Book(2, 'Be Fast with FastAPI', 'codingwithrobby', 'A great book', 5),
Book(3, 'Master Endpoints', 'codingwithrobby', 'An awesome book', 5),
Book(4, 'HP1', 'Author 1', 'Book Description', 2),
Book(5, 'HP2', 'Author 2', 'Book Description', 3),
Book(6, 'HP3', 'Author 3', 'Book Description', 1)
]
@app.get("/books")
async def read_all_books():
return BOOKS
@app.post("/create_book")
async def create_book(book_request: BookRequest):
new_book = Book(**book_request.model_dump())
BOOKS.append(find_book_id(new_book))
def find_book_id(book: Book):
book.id = 1 if len(BOOKS) == 0 else BOOKS[-1].id + 1
return book