第 10 章 字段序列化与输出格式
上一章讲的是导出结果里要包含哪些字段:
- 用
model_dump()导出字典 - 用
model_dump_json()导出 JSON 字符串 - 用
include、exclude、exclude_none控制哪些字段导出
有时字段需要保留,但导出时要换一种表现形式:
published_at在模型内部是datetime,导出时想变成"2026-06-05 10:30"email在模型内部保存完整邮箱,导出时想变成脱敏邮箱tags在模型内部是list[str],导出时想变成一段文本views在模型内部是整数,导出时想变成"100 次阅读"
这不是输入校验,也不是把字段筛掉,而是用 field_serializer 控制导出值,同时保留模型内部原值。
本章学习目标
有时模型里的值没问题,导出给别人看时却需要换个样子。这章用 @field_serializer("字段名") 控制单字段输出:把 datetime 格式化成指定字符串,把邮箱做简单脱敏。做完后,你应能说清它与 field_validator 分别在什么时候起作用。
字段序列化基础
继续用文章详情做例子。为了把注意力放在序列化上,代码只保留本章用到的字段,并加上 published_at;前章的完整业务规则就不重复了。
现在我们关注这些字段:
published_at -> 发布时间,内部类型是 datetime
author.email -> 作者邮箱,内部保存完整值
tags -> 标签列表,内部类型是 list[str]
views -> 阅读量,内部类型是 int导出时则希望看到这些值:
published_at -> "2026-06-05 10:30"
author.email -> "x***g@example.com"
tags -> "Python, Pydantic, 序列化"
views -> "100 次阅读"内部保留便于 Python 处理的数据类型,到了导出这一步,再换成适合展示的格式。
先看它在输入和导出流程中的位置:
每个示例都是完整代码,可以复制到 .code/main.py 运行;下一步再用新示例覆盖它。
发布时间格式化
目标是模型内部保留 datetime,页面上显示为 "2026-06-05 10:30"。先比较默认导出和自定义格式,把下面的代码保存为 .code/main.py:
from datetime import datetime
from pydantic import BaseModel, Field, field_serializer
class ArticleDefault(BaseModel):
title: str = Field(min_length=2, max_length=50)
published_at: datetime
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
published_at: datetime
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")
data = {
"title": "Pydantic 字段序列化",
"published_at": "2026-06-05T10:30:00",
}
default_article = ArticleDefault.model_validate(data)
article = ArticleDetail.model_validate(data)
default_dict = default_article.model_dump()
article_dict = article.model_dump()
print(default_dict["published_at"], type(default_dict["published_at"]))
print(default_article.model_dump_json())
print(article.published_at, type(article.published_at))
print(article_dict["published_at"], type(article_dict["published_at"]))运行:
uv run python main.py输出如下:
2026-06-05 10:30:00 <class 'datetime.datetime'>
{"title":"Pydantic 字段序列化","published_at":"2026-06-05T10:30:00"}
2026-06-05 10:30:00 <class 'datetime.datetime'>
2026-06-05 10:30 <class 'str'>前两行来自没有序列化器的 ArticleDefault:model_dump() 保留 datetime 对象,model_dump_json() 则按默认规则输出 JSON 时间字符串。后两行来自 ArticleDetail:内部值依然是 datetime,model_dump() 的结果已经按指定格式变成字符串。想要这种展示格式,就需要 field_serializer。
model_dump() 的返回值
model_dump() 负责:
把模型对象导出成普通字典本身不会按本例的展示要求改写时间格式。
model_dump_json() 的返回值
model_dump_json() 负责:
把模型对象导出成 JSON 字符串它会按默认规则把 datetime 写成 JSON 时间字符串,但不会自动使用本例指定的格式。
field_serializer 的作用
field_serializer 则负责:
某个字段导出时,应该变成什么值例如:
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")所以导出 published_at 时,结果是格式化字符串,模型里的 datetime 不变。
输入校验与输出序列化流程
序列化器只在导出时生效,核心逻辑就是:
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")模型内部的 published_at 是 datetime,导出结果中的值则是字符串。
发布时间序列化流程
邮箱脱敏
邮箱例子里会同时用到校验器和序列化器,它们的分工很简单:
| 工具 | 发生阶段 | 主要作用 |
|---|---|---|
field_validator | 输入阶段 | 清理或检查输入值 |
field_serializer | 输出阶段 | 控制导出值 |
field_validator 检查输入,field_serializer 改写导出值。接着看邮箱脱敏:函数需要用户名和域名都存在,所以示例先检查这两个部分,再生成脱敏结果。这里不做完整的邮箱格式校验。模型内部仍保存完整邮箱。
将下面的完整代码保存为 .code/main.py:
from pydantic import BaseModel, Field, ValidationError, field_serializer, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("email")
@classmethod
def validate_email_structure(cls, value: str) -> str:
name, separator, domain = value.partition("@")
if not name or not separator or not domain or "@" in domain:
raise ValueError("邮箱必须包含用户名和域名")
return value
@field_serializer("email")
def serialize_email(self, value: str) -> str:
name, domain = value.split("@", 1)
if len(name) <= 2:
hidden_name = name[0] + "*"
else:
hidden_name = name[0] + "***" + name[-1]
return hidden_name + "@" + domain
data = {
"username": "xiaoming",
"email": "xiaoming@example.com",
}
user = UserInfo.model_validate(data)
user_dict = user.model_dump()
print(user.email)
print(user_dict["email"])
try:
UserInfo.model_validate({"username": "xiaoming", "email": "@example.com"})
except ValidationError as exc:
print(exc.errors()[0]["msg"])运行:
uv run python main.py输出如下:
xiaoming@example.com
x***g@example.com
Value error, 邮箱必须包含用户名和域名前两行对比了内部值和导出值。第三行展示无用户名的 @example.com 在输入阶段就被拒绝了;问题在校验时发现,不会拖到导出时才报错。
模型内部值
print(user.email)输出的还是完整值。
导出值
print(user_dict["email"])这时才会看到脱敏结果。
因此,field_validator 只检查脱敏所需的邮箱结构,field_serializer 负责生成展示值;内部保存的邮箱不会被改动。
邮箱脱敏序列化流程
多字段序列化
把几个序列化器放进同一个模型,分别处理:
published_at的时间格式tags列表转文本views阅读量转展示文本author.email邮箱脱敏
将下面的完整代码保存为 .code/main.py:
from datetime import datetime
from pydantic import BaseModel, Field, field_serializer, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("email")
@classmethod
def validate_email_structure(cls, value: str) -> str:
name, separator, domain = value.partition("@")
if not name or not separator or not domain or "@" in domain:
raise ValueError("邮箱必须包含用户名和域名")
return value
@field_serializer("email")
def serialize_email(self, value: str) -> str:
name, domain = value.split("@", 1)
if len(name) <= 2:
hidden_name = name[0] + "*"
else:
hidden_name = name[0] + "***" + name[-1]
return hidden_name + "@" + domain
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
published_at: datetime
views: int = Field(default=0, ge=0)
tags: list[str] = Field(default_factory=list)
author: UserInfo
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")
@field_serializer("tags")
def serialize_tags(self, value: list[str]) -> str:
return ", ".join(value)
@field_serializer("views")
def serialize_views(self, value: int) -> str:
return f"{value} 次阅读"
data = {
"title": "Pydantic 字段序列化",
"content": "这是一篇介绍 field_serializer 的文章内容",
"published_at": "2026-06-05T10:30:00",
"views": 100,
"tags": ["Python", "Pydantic", "序列化"],
"author": {
"username": "xiaoming",
"email": "xiaoming@example.com",
},
}
article = ArticleDetail.model_validate(data)
print(article.published_at, type(article.published_at))
print(article.views, type(article.views))
print(article.tags, type(article.tags))
print(article.author.email)
print(article.model_dump())
print(article.model_dump_json(indent=2))运行:
uv run python main.py字典和 JSON 中都能看到类似结果:
{
"published_at": "2026-06-05 10:30",
"views": "100 次阅读",
"tags": "Python, Pydantic, 序列化",
"author": {
"username": "xiaoming",
"email": "x***g@example.com"
}
}这里只截取了相关字段,完整结果还包括 title 和 content。模型内部的 views 仍是整数,tags 仍是列表,author.email 也仍是完整邮箱。
本例是给页面展示用的,所以阅读量和标签都转成了文本。默认的 field_serializer 会同时作用于 model_dump() 和 model_dump_json()。如果某个接口要求 views 是数字、tags 是列表,就别直接把这份展示格式当作接口数据。
代码中几个字段各自的处理方式如下:
published_at:datetime 转字符串
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")tags:列表转展示文本
@field_serializer("tags")
def serialize_tags(self, value: list[str]) -> str:
return ", ".join(value)views:整数转展示文本
@field_serializer("views")
def serialize_views(self, value: int) -> str:
return f"{value} 次阅读"嵌套模型序列化器
author.email 的序列化器写在 UserInfo 里,但当 ArticleDetail 导出时,它仍然会一起生效。
导出外层模型时,嵌套模型里的序列化器也会执行。
多字段序列化结果对比
内部值与导出结果
序列化器不改变模型内部值
以 published_at: datetime 为例,访问 article.published_at 拿到的依旧是 datetime;只有调用 article.model_dump(),导出字典里的值才会变成字符串。为了看清两者的区别,运行下面的代码:
将下面的完整代码保存为 .code/main.py:
from datetime import datetime
from pydantic import BaseModel, Field, field_serializer
class ArticleDetail(BaseModel):
title: str
published_at: datetime
views: int = Field(default=0, ge=0)
tags: list[str] = Field(default_factory=list)
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")
@field_serializer("views")
def serialize_views(self, value: int) -> str:
return f"{value} 次阅读"
@field_serializer("tags")
def serialize_tags(self, value: list[str]) -> str:
return ", ".join(value)
data = {
"title": "Pydantic 字段序列化",
"published_at": "2026-06-05T10:30:00",
"views": 100,
"tags": ["Python", "Pydantic"],
}
article = ArticleDetail.model_validate(data)
article_dict = article.model_dump()
print(article.published_at, type(article.published_at))
print(article.views, type(article.views))
print(article.tags, type(article.tags))
print(article_dict["published_at"], type(article_dict["published_at"]))
print(article_dict["views"], type(article_dict["views"]))
print(article_dict["tags"], type(article_dict["tags"]))运行:
uv run python main.py六行结果先列模型内部值,再列对应的导出值:
2026-06-05 10:30:00 <class 'datetime.datetime'>
100 <class 'int'>
['Python', 'Pydantic'] <class 'list'>
2026-06-05 10:30 <class 'str'>
100 次阅读 <class 'str'>
Python, Pydantic <class 'str'>内部仍是 datetime、int、list;只有导出结果变成了 str。
文章字段序列化案例
最后把本章几种格式整理到一个例子里:
将下面的完整代码保存为 .code/main.py:
from datetime import datetime
from pydantic import BaseModel, Field, field_serializer, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("email")
@classmethod
def validate_email_structure(cls, value: str) -> str:
name, separator, domain = value.partition("@")
if not name or not separator or not domain or "@" in domain:
raise ValueError("邮箱必须包含用户名和域名")
return value
@field_serializer("email")
def serialize_email(self, value: str) -> str:
name, domain = value.split("@", 1)
if len(name) <= 2:
hidden_name = name[0] + "*"
else:
hidden_name = name[0] + "***" + name[-1]
return hidden_name + "@" + domain
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
published_at: datetime
views: int = Field(default=0, ge=0)
tags: list[str] = Field(default_factory=list)
author: UserInfo
@field_serializer("published_at")
def serialize_published_at(self, value: datetime) -> str:
return value.strftime("%Y-%m-%d %H:%M")
@field_serializer("tags")
def serialize_tags(self, value: list[str]) -> str:
return ", ".join(value)
@field_serializer("views")
def serialize_views(self, value: int) -> str:
return f"{value} 次阅读"
data = {
"title": "Pydantic 字段序列化",
"content": "这是一篇介绍 field_serializer 的文章内容",
"published_at": "2026-06-05T10:30:00",
"views": 100,
"tags": ["Python", "Pydantic", "序列化"],
"author": {
"username": "xiaoming",
"email": "xiaoming@example.com",
},
}
article = ArticleDetail.model_validate(data)
article_dict = article.model_dump()
article_json = article.model_dump_json(indent=2)
print(article.published_at, type(article.published_at))
print(article.views, type(article.views))
print(article.tags, type(article.tags))
print(article.author.email)
print(article_dict)
print(article_dict["author"]["email"])
print(article_json)运行:
uv run python main.py运行结果里,模型内部的阅读量还是 100,作者邮箱还是 xiaoming@example.com;导出的字典和 JSON 则使用格式化后的值:
100 <class 'int'>
xiaoming@example.com
x***g@example.com"views": "100 次阅读",
"email": "x***g@example.com"上面是输出节选,完整 JSON 还包含文章其他字段。
对照输出看两件事:内部值保留原类型,导出时才应用展示格式;model_dump() 和 model_dump_json() 都会执行序列化器。
本章小结
想改字段导出的样子,用 field_serializer。它在导出阶段运行,模型内部保存的值不会因此改变。field_validator 负责输入时的检查和处理,field_serializer 负责输出时的格式。