Serialization and Validation
FastAPI is a modern, fast (hence the name) web framework for building APIs in Python. One of its core features is Pydantic, which is used for data validation and serialization. FastAPI leverages Pydantic to ensure that data passed to your application is valid and structured correctly. Let’s dive deeper into the key concepts: Pydantic Models, Data Validation, Response Models, Nested Models, and Custom Validators.
Pydantic Models
Pydantic is a data validation and parsing library that FastAPI uses to define and validate the structure of the data, both for incoming requests (body, query parameters, etc.) and outgoing responses.
A Pydantic model is a class that inherits from pydantic.BaseModel. These models define the shape of the data and automatically perform validation based on the types and constraints you specify.
Example: Basic Pydantic Model
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
- Attributes: The attributes
name,description,price, andtaxdefine the structure of an item. - Type Validation: FastAPI uses Python's type annotations to validate the data. For example,
name: strensures thenameis a string, andprice: floatensures thepriceis a floating-point number. - Optional Fields:
descriptionandtaxare optional, as they have default values ofNone.
Data Validation
One of the primary benefits of using Pydantic models in FastAPI is automatic data validation. When a request body is passed to a FastAPI endpoint, FastAPI will validate it against the Pydantic model. If the data doesn't match the expected structure, FastAPI will return an error with a detailed message.
Example: Automatic Data Validation
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
description: str = None
tax: float = None
@app.post("/items/")
def create_item(item: Item):
return {"item_name": item.name, "item_price": item.price}
- When a client sends a request to
POST /items/with invalid data (e.g., missing required fields), FastAPI will return a 422 Unprocessable Entity error with details about which fields are missing or invalid.
Example Request (Invalid JSON)
{
"name": "Laptop"
}
Response (Error Message)
{
"detail": [
{
"loc": ["body", "price"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
In this case, the price field is required but missing, so FastAPI returns a 422 status with an error message indicating the missing field.
Response Models
FastAPI allows you to use Pydantic models to return structured data from your endpoints. This ensures that the response data is properly validated and formatted.
By default, FastAPI automatically serializes the response to JSON based on the Pydantic model.
Example: Using Response Models
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class Item(BaseModel):
name: str
price: float
description: str = None
tax: float = None
@app.post("/items/", response_model=Item)
def create_item(item: Item):
return item # Returning the created item as a response
- Response Model: By specifying
response_model=Itemin the@app.post()decorator, FastAPI will ensure that the response data conforms to theItemPydantic model. This automatically validates and serializes the response before sending it back to the client. - The response will be a JSON object with the same structure as the
Itemmodel.
Example Response:
{
"name": "Laptop",
"price": 999.99,
"description": "High-end laptop",
"tax": 50.99
}
If you return extra or invalid fields in the response, FastAPI will automatically handle them according to the model.
Nested Models
Pydantic models can be nested inside other models to represent complex data structures. This is especially useful for representing hierarchical or related data.
Example: Nested Models
from pydantic import BaseModel
class Category(BaseModel):
name: str
description: str
class Item(BaseModel):
name: str
price: float
category: Category
@app.post("/items/", response_model=Item)
def create_item(item: Item):
return item
- Nested Model: In this example, the
Itemmodel contains a nestedCategorymodel, which itself hasnameanddescriptionattributes. - Automatic Validation: FastAPI will recursively validate the nested model. In this case, it will validate both
Itemand theCategorynested inside it.
Example Request (JSON)
{
"name": "Laptop",
"price": 999.99,
"category": {
"name": "Electronics",
"description": "Devices and gadgets"
}
}
Example Response (JSON)
{
"name": "Laptop",
"price": 999.99,
"category": {
"name": "Electronics",
"description": "Devices and gadgets"
}
}
Custom Validators
While Pydantic provides built-in validation for common data types, you can also create custom validators for more complex validation logic. This allows you to ensure that the incoming data meets your application's specific requirements.
Example: Using Custom Validators
You can define custom validation logic using the @validator decorator in Pydantic models. Validators allow you to add custom constraints or modify the data before it’s used.
from pydantic import BaseModel, validator
class Item(BaseModel):
name: str
price: float
description: str = None
# Custom validator for price
@validator("price")
def validate_price(cls, value):
if value <= 0:
raise ValueError("Price must be greater than 0")
return value
@app.post("/items/")
def create_item(item: Item):
return item
- Custom Validator: The
@validator("price")decorator allows you to define a custom validation function for thepricefield. In this case, it checks that the price is greater than 0. If the validation fails, aValueErroris raised, and FastAPI will return a 422 error.
Example Request (Invalid JSON)
{
"name": "Laptop",
"price": -999.99
}
Response (Error Message)
{
"detail": [
{
"loc": ["body", "price"],
"msg": "Price must be greater than 0",
"type": "value_error"
}
]
}
Additional Validator Example: String Length Validation
You can also validate string lengths with custom validators.
class Item(BaseModel):
name: str
description: str
@validator("name")
def name_length(cls, value):
if len(value) < 3:
raise ValueError("Name must be at least 3 characters long")
return value
- This custom validator ensures that the
namefield is at least 3 characters long. If the condition fails, it raises an error.