Pydantic & FastAPI: Optional Fields, Nested Models, and Advanced Validation
Learn how to leverage Pydantic in FastAPI to handle optional fields, validate nested data structures, enforce complex business rules with model validators, forbid extra fields, work with polymorphic models, and validate query and path parameters, all illustrated with clear Python code examples.
Handling Optional Fields
When building a user profile creation API, some fields such as username and email are required, while others like bio and age are optional. Optional fields enrich the profile but are not mandatory.
<code>from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class UserProfile(BaseModel):
username: str # 必填字段
email: str # 必填字段
bio: Optional[str] = None # 可选字段,默认值为 None
age: Optional[int] = None # 可选字段
@app.post("/profile")
async def create_profile(profile: UserProfile):
return {"message": "Profile created", "profile_data": profile.model_dump()}
</code>We define optional fields using Python's typing.Optional . If the user provides them, they will be validated.
model_dump is what?
model_dump returns the model data as a dictionary, suitable for JSON serialization, including all fields with default values or exclusions applied.
Nested Models for Complex Request Validation
For APIs that need to validate complex JSON structures, such as a user profile containing nested details (address, preferences, contact information), Pydantic supports nested models to simplify validation.
<code>from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field
app = FastAPI()
class Address(BaseModel):
street: str = Field(..., min_length=5) # 街道名称,最小长度为 5
city: str
zipcode: str = Field(..., regex=r"^\d{5}$") # 验证美国风格的邮政编码
class Preferences(BaseModel):
newsletter: bool # 布尔字段,用于订阅新闻通讯
sms_notifications: bool # 布尔字段,用于短信通知
class ContactInfo(BaseModel):
email: EmailStr # 验证电子邮件格式
phone: str = Field(..., regex=r"^\+?[1-9]\d{1,14}$") # 验证国际电话号码
class UserProfile(BaseModel):
username: str = Field(..., min_length=3, max_length=50) # 用户名长度在 3 到 50 之间
password: str = Field(..., min_length=8) # 密码最小长度为 8
address: Address # 嵌套的 Address 模型
preferences: Preferences # 嵌套的 Preferences 模型
contact_info: ContactInfo # 嵌套的 ContactInfo 模型
@app.post("/create-profile")
async def create_profile(profile: UserProfile):
return {"message": "Profile created successfully!", "username": profile.username}
</code>In this example, Address , Preferences , and ContactInfo are nested inside UserProfile and are automatically validated by Pydantic.
Enforcing Complex Validation Rules
An event creation API must ensure that end_time is later than start_time and that maximum_attendees does not exceed a specified limit.
<code>from fastapi import FastAPI
from pydantic import BaseModel, Field, model_validator
from datetime import datetime
from typing import Optional
app = FastAPI()
class Event(BaseModel):
name: str = Field(..., min_length=3, max_length=100)
start_time: datetime
end_time: datetime
maximum_attendees: Optional[int] = Field(None, ge=1, le=1000)
@model_validator(mode="after")
def validate_event_times(cls, values):
start_time = values.get('start_time')
end_time = values.get('end_time')
if start_time and end_time and end_time <= start_time:
raise ValueError("End time must be later than start time.")
return values
@app.post("/create-event")
async def create_event(event: Event):
return {"message": "Event created successfully!", "event": event.model_dump()}
</code>The model_validator ensures end_time is after start_time , and the maximum_attendees field is constrained between 1 and 1000.
model_validator is what?
model_validator validates the entire model, allowing cross‑field checks and adjustments during the model lifecycle.
Forbidding Extra Fields
When building an API that should only accept specific fields, any additional fields must be strictly rejected.
<code>from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserProfile(BaseModel):
username: str
email: str
class Config:
extra = "forbid" # 禁止意外字段
@app.post("/create-profile")
async def create_profile(profile: UserProfile):
return {"message": "Profile created", "data": profile.model_dump()}
</code>Setting extra = "forbid" in the model’s Config class causes FastAPI to reject requests containing fields not defined in the model.
Config is what?
Config is a special internal class that customizes model behavior, allowing options that affect validation, serialization, and other aspects.
Handling Polymorphic Models
For APIs that accept multiple user types, such as AdminUser or NormalUser , both share common fields like username but have type‑specific fields.
<code>from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Union
app = FastAPI()
class AdminUser(BaseModel):
username: str
admin_level: int
class NormalUser(BaseModel):
username: str
age: int
class UserProfile(BaseModel):
user: Union[AdminUser, NormalUser] # "user" field can be AdminUser or NormalUser
@app.post("/create-user")
async def create_user(profile: UserProfile):
return {"message": "User created successfully!", "data": profile.model_dump()}
</code>The UserProfile model uses Union[AdminUser, NormalUser] so the correct model is chosen based on the incoming payload.
Query Parameters and Path Variables
In an API that searches users by a path variable user_id and allows query parameters, both need validation.
<code>from fastapi import FastAPI, Query, Path
app = FastAPI()
@app.get("/search/{user_id}")
async def search(
user_id: int = Path(..., gt=0), # 路径参数 user_id,确保它是正整数
query: str = Query(..., min_length=3, max_length=50) # 查询参数,长度验证
):
return {"message": "Search results", "user_id": user_id, "query": query}
</code>Here user_id must be a positive integer and query must be a string with length between 3 and 50, ensuring the API processes only valid input.
These are some of the most common and powerful ways to use Pydantic in a FastAPI application to simplify validation and ensure robust, clear data handling.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.