引言如果你在 Python 项目中使用 Pydantic 进行数据校验和序列化,那么model_dump几乎是你每天都会调用的方法。它是 Pydantic v2 中将模型实例转换为 Python 原生数据结构(通常是dict)的标准方式,也是连接「内部模型」和「外部世界」(JSON API、数据库、消息队列等)最重要的桥梁。但model_dump远不止.dict()那么简单。它有十多个参数,每一个都对应着真实工程中的一种常见需求:部分序列化、字段重命名、忽略默认值、处理多态、嵌套模型、循环结构……不理解这些参数,你的代码很可能在某个边界条件下悄悄出错。本文将系统性地剖析model_dump,从历史背景讲到每一个参数的语义、典型用例、容易踩的坑,并配套大量代码示例。一、历史背景:从.dict()到model_dump在 Pydantic v1 时代,我们用instance.dict()把模型转为字典,用instance.json()转为 JSON 字符串。这套 API 简洁直观,但存在几个深层问题:命名冲突。dict是 Python 内置类型,如果模型本身有一个叫dict的字段(比如配置项),方法名就会被字段遮蔽。职责不清。.dict()既要做「Python 序列化」,又要给.json()做底层准备,两种语义混在一起。扩展困难。v1 的实现基于 Python 反射,性能有上限;v2 的核心序列化器用 Rust 重写(pydantic-core),需要全新的接口。于是 v2 引入了带model_前缀的一整套方法:model_dump、model_dump_json、model_validate、model_copy等等。前缀本身就是一种命名空间,彻底避开了与字段名冲突的风险。# Pydantic v1user.dict()user.json()# Pydantic v2user.model_dump()user.model_dump_json()如果你正在维护从 v1 迁移过来的代码,Pydantic 提供了bump-pydantic工具帮助自动改写。但理解新方法的语义仍然是必修课——v2 不只是改了名字,行为细节也有不少变化。二、model_dump的完整签名先把全貌摆出来:defmodel_dump(self,*,mode:Literal[json,python]|strpython,include:IncEx|NoneNone,exclude:IncEx|NoneNone,context:Any|NoneNone,by_alias:boolFalse,exclude_unset:boolFalse,exclude_defaults:boolFalse,exclude_none:boolFalse,round_trip:boolFalse,warnings:bool|Literal[none,warn,error]True,serialize_as_any:boolFalse,)-dict[str,Any]:...注意所有参数都是关键字参数(*之后),这是 v2 的强制约定,目的是让调用点可读、避免位置参数的歧义。下面我们逐个剖析。三、最朴素的用法frompydanticimportBaseModelfromdatetimeimportdatetimeclassUser(BaseModel):id:intname:strcreated_at:datetime userUser(id1,nameAlice,created_atdatetime(2026,1,15,10,30))print(user.model_dump())输出:{id:1,name:Alice,created_at:datetime.datetime(2026,1,15,10,30)}注意created_at仍然是datetime对象,不是字符串。这是默认modepython的关键特性:保留 Python 原生类型。如果你接下来要把这个字典塞给json.dumps,会立刻报错。这就引出了下一个参数。四、mode:Python 模式 vs JSON 模式mode决定了字段值被序列化成什么样子的 Python 对象。modepython(默认)输出值仍然是 Python 类型:datetime还是datetime,UUID还是UUID,Decimal还是Decimal,自定义枚举还是枚举成员。这适合下游还需要在 Python 里继续处理这些对象的场景。modejson输出值是「JSON 兼容的 Python 类型」——也就是str、int、float、bool、None、list、dict这几种。datetime会变成 ISO 8601 字符串,UUID变成字符串,Decimal变成字符串(为避免精度损失),枚举变成它的value。fromuuidimportuuid4fromdecimalimportDecimalfromenumimportEnumclassStatus(str,Enum):ACTIVEactiveINACTIVEinactiveclassOrder(BaseModel):id:intamount:Decimal status:Status placed_at:datetime orderOrder(id1,amountDecimal(19.99),statusStatus.ACTIVE,placed_atdatetime(2026,4,28,12,0),)print(order.model_dump(modepython))# {id: 1, amount: Decimal(19.99),# status: Status.ACTIVE: active, placed_at: datetime(...)}print(order.model_dump(modejson))# {id: 1, amount: 19.99,# status: active, placed_at: 2026-04-28T12:00:00}一个常见的反模式很多人写 API 时这样写:returnjson.dumps(model.model_dump())# ❌ 在 datetime/Decimal 上崩溃正确写法有两种:# 方案 A:用 model_dump_json 直接产出 JSON 字符串(最快、最准)returnmodel.model_dump_json()# 方案 B:如果还需要先操作 dict 再序列化,用 modejsondatamodel.model_dump(modejson)data[extra_field]somethingreturnjson.dumps(data)model_dump_json内部走 Rust 实现的快速路径,比「先model_dump再json.dumps」快很多,所以只要不需要中间修改字典,就优先用model_dump_json。五、include与exclude:精细控制输出字段这两个参数互为镜像,接受三种形式:字段名的集合:{id, name}字段名到嵌套规则的字典:{id: True, address: {city: True}}整数索引(用于列表元素)classAddress(BaseModel):street:strcity:strzipcode:strclassUser(BaseModel):id:intname:stremail:strpassword_hash:straddress:Address userUser(id1,nameAlice,emailax.com,password_hash...,addressAddress(street1 Main St,cityBoston,zipcode02101),)# 只输出 id 和 nameuser.model_dump(include{id,name})# {id: 1, name: Alice}# 排除敏感字段user.model_dump(exclude{password_hash})# 嵌套控制:address 里只保留 cityuser.model_dump(include{id:True,address:{city}})# {id: 1, address: {city: Boston}}处理列表/字典字段当字段是list或dict时,用整数索引或键来定位元素,或者用__all__表示「所有元素」:classTeam(BaseModel):members:list[User]teamTeam(members[user,user])# 所有成员都只保留 nameteam.model_dump(include{members:{__all__:{name}}})# {members: [{name: Alice}, {name: Alice}]}# 只保留第 0 个成员的 nameteam.model_dump(include{members:{0:{name}}})容易踩的坑include和exclude同时传时,exclude优先级更高。各种过滤是「与」关系。include不会让被exclude_unset过滤掉的字段重新出现。如果同一套字段过滤规则在多处被复用,把它提到模块级常量:PUBLIC_FIELDS {id, name, email},不要每次手写。include/exclude是运行时的过滤;如果某些字段「永远不应输出」(比如密码哈希),更稳妥的做法是用Field(excludeTrue)在模型定义层面就声明,这样不会因为某次调用忘了传exclude而泄露。六、by_alias:别名输出Pydantic 允许字段定义别名,常见于「内部用 snake_case,对外吐 camelCase」的场景:frompydanticimportBaseModel,FieldclassUser(BaseModel):user_id:intField(aliasuserId)full_name:strField(aliasfullName)userUser(userId1,fullNameAlice)# 输入用别名print(user.model_dump())# {user_id: 1, full_name: Alice}print(user.model_dump(by_aliasTrue))# {userId: 1, fullName: Alice}更优雅的做法是用别名生成器统一处理:frompydanticimportBaseModel,ConfigDictfrompydantic.alias_generatorsimportto_camelclassAPIModel(BaseModel):model_configConfigDict(alias_generatorto_camel,populate_by_nameTrue,# 同时允许用原名输入)classUser(APIModel):user_id:intfull_name:strUser(user_id1,full_nameAlice).model_dump(by_aliasTrue)# {userId: 1, fullName: Alice}这是 FastAPI 项目里非常常见的写法——内部模型用 Pythonic 命名,对外接口自动 camelCase。提示:AliasChoices与AliasPath在 v2 里 alias 还可以是更复杂的形式(如AliasChoices(userId, uid)接受多个候选键)。但这些只影响输入(校验);序列化时by_aliasTrue用的始终是serialization_alias或alias(优先取前者)。如果你需要「输入接受 A、输出固定为 B」,就显式分开设置validation_alias和serialization_alias。七、exclude_*三连:三种「省略」语义这一组参数容易混淆,但语义其实非常清晰——它们对应三个不同问题:exclude_unsetTrue:「用户没传的字段就不要输出」只输出那些显式赋值过的字段,无论它的值是否等于默认值。这对部分更新(PATCH)语义至关重要。classUpdateUser(BaseModel):name:str|NoneNoneemail:str|NoneNoneage:int|NoneNone# 用户只想改 emailpatchUpdateUser(emailnewx.com)patch.model_dump()# {name: None, email: newx.com, age: None} ← 默认值被吐出来了patch.model_dump(exclude_unsetTrue)# {email: newx.com} ← 这才是 PATCH 该发的 payload如果不用exclude_unset,你的 PATCH 会把所有未传字段全部覆盖成None——这是把数据库洗成一片空白的经典 bug。exclude_defaultsTrue:「值等于默认值就不要输出」classConfig(BaseModel):timeout:int30retries:int3debug:boolFalsecConfig(timeout60)# 只改 timeoutc.model_dump(exclude_defaultsTrue)# {timeout: 60}注意它和exclude_unset的区别:即使你显式传了Config(timeout30),因为值等于默认值,exclude_defaults也会把它过滤掉;而exclude_unset会保留它。exclude_noneTrue:「值是None就不要输出」classProfile(BaseModel):name:strnickname:str|NoneNonepProfile(nameAlice)p.model_dump(exclude_noneTrue)# {name: Alice}适合输出给那些「字段缺失」和「字段为 null」语义不同的下游(很多 NoSQL、配置系统、前端框架都对 null 敏感)。一张速查表参数过滤条件典型场景exclude_unset用户没显式赋值PATCH 半量更新exclude_defaults值 默认值配置文件输出最简版exclude_none值是None兼容拒绝 null 的下游三者可以叠加,效果是「或」(任一条件满足即过滤掉)。八、round_trip:可往返序列化这个参数的存在是因为 Pydantic 的某些校验/序列化是有损的。最经典的例子是判别式联合(discriminated union)和带Json类型的字段。frompydanticimportBaseModel,JsonclassEvent(BaseModel):payload:Json[dict]eEvent(payload{k: 1})# 输入是字符串print(e.payload)# {k: 1} ← 已被解析e.model_dump()# {payload: {k: 1}} ← 默认输出解析后的 dicte.model_dump(round_tripTrue)# {payload: {k: 1}} ← 输出字符串,可以再次喂给校验器判断要不要用它的规则很简单:如果你打算把 dump 出来的数据再喂回model_validate重建模型,就开round_tripTrue;否则用默认。九、warnings:控制序列化警告当 Pydantic 在序列化时发现「这个值类型对不上字段声明」之类的问题,默认会发警告(用 Pythonwarnings模块)。warnings参数可以改变这个行为:True(默认):发出警告。False或none:静默忽略。error:把警告升级成PydanticSerializationError异常。在生产环境我强烈推荐至少在测试套件里开warningserror——它能把许多潜在的类型不匹配问题暴露在 CI 阶段,而不是某个生产请求的边界数据触发它。classItem(BaseModel):quantity:int# 假设由于某种原因实例字段被错误地塞了字符串(比如绕过校验直接 setattr)itemItem.model_construct(quantityabc)# construct 不做校验item.model_dump(warningserror)# 抛出 PydanticSerializationError十、serialize_as_any与多态序列化这是一个 v2 中相对较新的、解决「子类字段被截断」问题的参数。问题场景frompydanticimportBaseModelclassAnimal(BaseModel):name:strclassDog(Animal):breed:strclassOwner(BaseModel):pet:Animal# ← 声明的是父类ownerOwner(petDog(nameRex,breedHusky))owner.model_dump()# {pet: {name: Rex}} ← breed 字段消失了!默认情况下,Pydantic 序列化嵌套模型时会按声明类型(Animal)序列化,丢掉Dog独有的字段。这种「鸭子类型截断」在大多数静态语言里是正确行为,但在 Python 里经常违反直觉。解决方案owner.model_dump(serialize_as_anyTrue)# {pet: {name: Rex, breed: Husky}} ← 按运行时类型序列化或者把声明改成pet: Any,效果类似但更宽松。如果整个项目都希望按运行时类型序列化,可以在配置里开启:classAnimal(BaseModel):model_configConfigDict(serialize_by_aliasTrue)# (示例)判别式联合(Union[Dog, Cat]配合Field(discriminatorkind))是另一种处理多态的更严谨方式,适合 API 边界明确的场景。十一、context:把外部信息传给序列化器context是一个透传字典,会被传到自定义序列化器里。它让序列化逻辑可以根据运行时信息动态调整——比如「当前用户的角色」「当前请求的语言」。frompydanticimportBaseModel,SerializationInfo,field_serializerclassDocument(BaseModel):content:strsecret_notes:strfield_serializer(secret_notes)defserialize_notes(self,value:str,info:SerializationInfo)-str:ctxinfo.contextor{}ifctx.get(role)admin:returnvaluereturn[REDACTED]docDocument(contentHello,secret_notesInternal only)doc.model_dump(context{role:user})# {content: Hello, secret_notes: [REDACTED]}doc.model_dump(context{role:admin})# {content: Hello, secret_notes: Internal only}这比「定义两个不同的模型」或「用include/exclude配合一堆 if-else」干净得多。context在 i18n、字段权限、单位换算(用户偏好公制还是英制)等场景里非常好用。十二、自定义序列化:field_serializer和model_serializermodel_dump的最终输出可以被开发者完全接管。Pydantic v2 提供两个装饰器:field_serializer:针对单个字段frompydanticimportBaseModel,field_serializerfromdatetimeimportdatetimeclassEvent(BaseModel):name:stroccurred_at:datetimefield_serializer(occurred_at)defserialize_dt(self,dt:datetime)-str:returndt.strftime(%Y/%m/%d %H:%M)Event(namelaunch,occurred_atdatetime(2026,4,28,12,0)).model_dump()# {name: launch, occurred_at: 2026/04/28 12:00}可以指定modewrap让你「先调用默认逻辑、再加工」;也可以指定when_used控制只在某些 mode 下生效(如when_usedjson)。model_serializer:接管整个模型frompydanticimportBaseModel,model_serializerclassMoney(BaseModel):amount:floatcurrency:strmodel_serializerdefto_compact(self)-str:returnf{self.amount:.2f}{self.currency}Money(amount19.99,currencyUSD).model_dump()# 19.99 USD ← 整个模型变成一个字符串!注意model_dump的返回类型签名是dict[str, Any],但用model_serializer后实际返回值可以是任意类型——这是个静态类型上的小妥协,实际开发中需要注意。十三、嵌套模型:序列化是递归的model_dump会递归处理嵌套的BaseModel、list、dict、set、tuple。所有参数(mode、exclude_*、by_alias等)都会沿着递归路径传播。classInner(BaseModel):value:int0note:str|NoneNoneclassOuter(BaseModel):items:list[Inner]oOuter(items[Inner(value1),Inner(value0,notehi)])o.model_dump(exclude_defaultsTrue)# {items: [{value: 1}, {note: hi}]}# 注意第二个元素的 value0 因为是默认值被剔除这种「全局规则下沉」的设计极大减少了样板代码,但也意味着你不能针对不同层级用不同的规则。如果真的需要,要么在子模型里写自定义序列化器,要么 dump 出来后手动加工。十四、model_dumpvsmodel_dump_json它们的参数列表几乎完全一致,但语义上有重要区别:维度model_dumpmodel_dump_json返回类型dict[str, Any](或自定义序列化器返回的类型)str默认 modepython(可选json)始终是 JSON性能中等极快(纯 Rust 路径)用途还要在 Python 里继续处理直接发送/落盘经验法则:要发 HTTP 响应、写日志、推消息队列 →model_dump_json()。要继续修改、传给pandas.DataFrame、和别的 dict 合并 →model_dump(modejson)或model_dump()。十五、性能与最佳实践性能要点能用model_dump_json就别用json.dumps(model.model_dump())。后者要做两次类型转换,前者一次完成。避免在循环里反复 dump 同一个模型。如果数据没变,缓存 dump 结果。对超大批量数据,考虑用TypeAdapter。比如要序列化list[User],直接TypeAdapter(list[User]).dump_python(users)比逐个user.model_dump()然后拼列表要快。工程化最佳实践敏感字段用Field(excludeTrue)而不是依赖调用方传exclude。前者是声明式安全,后者是约定式安全。对外 API 用by_aliasTrue,内部用 snake_case。配合to_camel别名生成器。PATCH 接口必须用exclude_unsetTrue。这是数据安全的硬规则。测试里把warningserror当默认。让序列化警告早暴露。多态字段慎用父类标注。要么显式Union加 discriminator,要么 dump 时记得serialize_as_anyTrue。优先model_dump_json而不是modejsonjson.dumps,除非真的需要中间字典。十六、一个综合案例把上面的知识串起来,看一个接近真实项目的例子:一个用户资料的 PATCH 接口。fromdatetimeimportdatetimefrompydanticimportBaseModel,ConfigDict,Field,field_serializerfrompydantic.alias_generatorsimportto_camelclassAPIModel(BaseModel):model_configConfigDict(alias_generatorto_camel,populate_by_nameTrue,)classUserPatch(APIModel):display_name:str|NoneNoneemail:str|NoneNonebio:str|NoneNonepassword_hash:str|NoneField(defaultNone,excludeTrue)# 永不输出last_login:datetime|NoneNonefield_serializer(last_login)defserialize_last_login(self,dt:datetime|None)-str|None:returndt.isoformat()ifdtelseNone# 只想改昵称patchUserPatch(displayNameAlice2026)# 发给后端时:exclude_unset 保证只发用户改过的;by_alias 输出 camelCasepayloadpatch.model_dump_json(exclude_unsetTrue,by_aliasTrue)# {displayName:Alice2026}# 即使有人不小心给 patch.password_hash 赋值,也不会被序列化输出这一行model_dump_json综合用了 4 个特性:JSON 输出、字段过滤、声明式排除、别名转换。这就是model_dump系列方法在真实项目中的样子——参数不是用来记的,是用来组合的。结语model_dump看上去只是「把模型变成字典」,但它实际上是 Pydantic 暴露给开发者的一个高度可组合的序列化引擎。理解它的每一个参数,本质上是在理解「我打算把数据交给谁、对方期望什么样子」这个工程问题。下次你写下.model_dump()时,不妨多问一句:接收方是 Python 还是 JSON?(mode)是全量还是增量?(exclude_unset)内部命名还是对外命名?(by_alias)多态吗?(serialize_as_any)这数据待会儿要再喂回模型吗?(round_trip)把这些问题答清楚,你写出的序列化代码就不会再有「为什么生产环境少了一个字段」式的玄学问题。