告别千篇一律:Pydantic高级序列化的艺术与实战
你是否还在为API返回的JSON格式呆板而烦恼?是否遇到过日期格式不统一、敏感字段泄露或复杂数据类型序列化失败的问题?本文将带你掌握Pydantic序列化的高级技巧,通过5个实战案例,让你的JSON输出既安全又优雅。读完本文,你将学会如何定制字段序列化、处理嵌套模型、动态过滤敏感数据,以及优化复杂数据结构的输出格式。
序列化基础与挑战
Pydantic作为数据验证领域的事实标准,其序列化功能同样强大。默认情况下,Pydantic模型通过model_dump()和model_dump_json()方法提供基础的序列化能力,但在实际开发中,我们常常需要更灵活的控制。
官方文档中详细介绍了默认序列化行为,包括Python模式和JSON模式的区别。Python模式会递归转换模型为字典,但可能包含非JSON兼容类型;JSON模式则确保输出符合JSON规范。例如:
from pydantic import BaseModel
class Event(BaseModel):
timestamp: datetime
participants: set[str]
event = Event(timestamp=datetime(2023, 1, 1), participants={'alice', 'bob'})
print(event.model_dump()) # Python模式:包含datetime和set
print(event.model_dump(mode='json')) # JSON模式:datetime转为ISO字符串,set转为列表
字段级序列化:精细控制输出
Pydantic提供两种字段序列化方式:Plain Serializer(直接替换默认序列化)和Wrap Serializer(包装默认序列化),分别适用于不同场景。
Plain Serializer:完全自定义
当需要完全控制字段输出格式时,可使用PlainSerializer。例如将列表转换为逗号分隔的字符串:
from typing import Annotated
from pydantic import BaseModel, PlainSerializer
# 定义可复用的序列化类型
CommaSeparatedStr = Annotated[
list[str],
PlainSerializer(lambda x: ','.join(x), return_type=str)
]
class Book(BaseModel):
title: str
tags: CommaSeparatedStr
book = Book(title='Python 101', tags=['programming', 'python'])
print(book.model_dump()) # {'title': 'Python 101', 'tags': 'programming,python'}
Wrap Serializer:增强默认行为
当需要在默认序列化基础上添加额外处理时,WrapSerializer是更好的选择。例如为数值字段添加单位:
from typing import Annotated
from pydantic import BaseModel, WrapSerializer, SerializerFunctionWrapHandler
def add_currency(value: float, handler: SerializerFunctionWrapHandler) -> str:
return f"${handler(value):.2f}"
Price = Annotated[float, WrapSerializer(add_currency)]
class Product(BaseModel):
name: str
price: Price
product = Product(name='Laptop', price=999.99)
print(product.model_dump()) # {'name': 'Laptop', 'price': '$999.99'}
装饰器语法:模型内定义
对于模型特定的序列化逻辑,推荐使用@field_serializer装饰器:
from pydantic import BaseModel, field_serializer
class User(BaseModel):
join_date: datetime
roles: set[str]
@field_serializer('join_date')
def serialize_date(self, value: datetime) -> str:
return value.strftime('%Y-%m-%d')
@field_serializer('roles', mode='wrap')
def serialize_roles(self, value: set[str], handler: SerializerFunctionWrapHandler) -> list[str]:
return sorted(handler(value)) # 先调用默认序列化(set转list)再排序
模型级序列化:整体结构重塑
当需要控制整个模型的输出结构时,@model_serializer装饰器允许我们完全重定义序列化行为。
场景1:数据脱敏输出
from pydantic import BaseModel, model_serializer
class User(BaseModel):
username: str
email: str
password: str # 敏感字段
@model_serializer
def serialize_model(self) -> dict:
return {
'username': self.username,
'email': self.email.split('@')[0] + '***' # 隐藏邮箱域名
}
user = User(username='alice', email='alice@example.com', password='secret')
print(user.model_dump()) # {'username': 'alice', 'email': 'alice***'}
场景2:嵌套结构扁平化
class Address(BaseModel):
street: str
city: str
zipcode: str
class Customer(BaseModel):
name: str
address: Address
@model_serializer(mode='wrap')
def serialize_model(self, handler: SerializerFunctionWrapHandler) -> dict:
data = handler(self) # 获取默认序列化结果
# 扁平化地址字段
data.update(data.pop('address'))
return data
customer = Customer(name='Bob', address=Address(street='Main St', city='NY', zipcode='10001'))
print(customer.model_dump())
# {'name': 'Bob', 'street': 'Main St', 'city': 'NY', 'zipcode': '10001'}
高级技巧:上下文感知与动态调整
Pydantic支持通过context参数传递动态信息,实现同一模型根据不同场景输出不同格式。
示例:多版本API兼容
from pydantic import BaseModel, FieldSerializationInfo, field_serializer
class Product(BaseModel):
id: int
name: str
price: float
@field_serializer('price')
def serialize_price(self, value: float, info: FieldSerializationInfo) -> float | str:
# 根据上下文决定是否添加货币符号
if info.context and info.context.get('api_version') == 'v2':
return f"${value:.2f}"
return value
product = Product(id=1, name='Phone', price=999.99)
print(product.model_dump(context={'api_version': 'v1'})) # {'id': 1, 'name': 'Phone', 'price': 999.99}
print(product.model_dump(context={'api_version': 'v2'})) # {'id': 1, 'name': 'Phone', 'price': '$999.99'}
实战案例:电商订单API优化
让我们综合运用上述技巧,优化一个电商订单的API响应:
from datetime import datetime
from typing import Annotated, set
from pydantic import BaseModel, PlainSerializer, field_serializer, model_serializer
# 1. 定义可复用的序列化类型
Price = Annotated[float, PlainSerializer(lambda x: f"${x:.2f}", return_type=str)]
CommaList = Annotated[set[str], PlainSerializer(lambda x: ','.join(sorted(x)), return_type=str)]
# 2. 子模型定义
class OrderItem(BaseModel):
product_id: int
name: str
quantity: int
unit_price: Price # 使用自定义价格类型
@field_serializer('quantity', 'unit_price')
def add_labels(self, value, info: FieldSerializationInfo):
if info.field_name == 'quantity':
return f"{value}件"
return value # 价格已由Price类型处理
# 3. 主模型定义
class Order(BaseModel):
order_id: str
items: list[OrderItem]
created_at: datetime
status: str
tags: CommaList # 使用自定义列表类型
internal_notes: str = '' # 内部字段,默认不输出
@model_serializer(mode='wrap')
def serialize_order(self, handler: SerializerFunctionWrapHandler, info: SerializationInfo):
data = handler(self)
# 4. 根据上下文决定是否包含内部字段
if info.context and info.context.get('include_internal'):
data['internal_notes'] = self.internal_notes
else:
data.pop('internal_notes', None)
# 5. 添加计算字段
data['total_amount'] = sum(float(item['unit_price'][1:]) * int(item['quantity'][:-1])
for item in data['items'])
return data
# 使用示例
order = Order(
order_id='ORD-001',
items=[
OrderItem(product_id=1, name='键盘', quantity=2, unit_price=99.99),
OrderItem(product_id=2, name='鼠标', quantity=1, unit_price=49.99)
],
created_at=datetime(2023, 10, 1),
status='paid',
tags={'electronics', 'promotion'},
internal_notes='客户要求加急发货'
)
# 普通用户视角
print(order.model_dump(mode='json', indent=2))
# 管理员视角(包含内部信息)
print(order.model_dump(context={'include_internal': True}, indent=2))
性能与最佳实践
1.** 优先使用字段级序列化 :相比模型级序列化,字段级序列化性能更好且更易维护 2. 复用序列化逻辑 :通过Annotated创建可复用的序列化类型,如本文的Price和CommaList 3. 注意类型安全 :始终指定return_type,帮助Pydantic进行类型检查 4. 谨慎使用动态排除 **:频繁使用exclude/include参数可能导致API契约不明确,优先通过字段定义控制
通过本文介绍的Pydantic高级序列化技巧,你可以轻松实现灵活、安全且易于维护的API输出。无论是简单的格式转换还是复杂的动态输出,Pydantic都能提供优雅的解决方案。更多高级用法请参考官方序列化文档。
如果你觉得本文对你有帮助,请点赞收藏,关注获取更多Pydantic实战技巧!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




