序列化
除了通过字段名称(例如 model.foobar
)直接访问模型属性外,模型还可以通过多种方式进行转换、转储、序列化和导出。序列化可以为整个模型定制,也可以在每个字段或每种类型的基础上进行定制。
Serialize(序列化) 与 dump(转储)
Pydantic 将“serialize”(序列化)和“dump”(转储)这两个术语互换使用。它们都指将模型转换为字典或 JSON 编码字符串的过程。
在 Pydantic 之外,“serialize”通常指将内存中的数据转换为字符串或字节。然而,在 Pydantic 的上下文中,将对象从更结构化的形式(如 Pydantic 模型、数据类等)转换为由 Python 内置类型(如 dict)组成的较不结构化的形式,这两者之间存在非常密切的关系。
虽然我们可以(并且偶尔会)通过使用“dump”来表示转换为原始类型,使用“serialize”来表示转换为字符串来区分这些场景,但出于实际目的,我们经常使用“serialize”这个词来指代这两种情况,即使它并不总是意味着转换为字符串或字节。
序列化数据¶
Pydantic 允许模型(以及任何使用类型适配器的其他类型)以两种模式进行序列化:Python 模式和 JSON 模式。Python 输出可能包含非 JSON 可序列化的数据(尽管这可以被模拟)。
Python 模式¶
当使用 Python 模式时,Pydantic 模型(以及类似模型的类型,如 dataclasses)(1) 将被(递归地)转换为字典。这可以通过使用 model_dump()
方法实现。
- 除了根模型外,根模型会直接转储其根值。
from typing import Optional
from pydantic import BaseModel, Field
class BarModel(BaseModel):
whatever: tuple[int, ...]
class FooBarModel(BaseModel):
banana: Optional[float] = 1.1
foo: str = Field(serialization_alias='foo_alias')
bar: BarModel
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': (1, 2)})
# returns a dictionary:
print(m.model_dump())
#> {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': (1, 2)}}
print(m.model_dump(by_alias=True))
#> {'banana': 3.14, 'foo_alias': 'hello', 'bar': {'whatever': (1, 2)}}
from pydantic import BaseModel, Field
class BarModel(BaseModel):
whatever: tuple[int, ...]
class FooBarModel(BaseModel):
banana: float | None = 1.1
foo: str = Field(serialization_alias='foo_alias')
bar: BarModel
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': (1, 2)})
# returns a dictionary:
print(m.model_dump())
#> {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': (1, 2)}}
print(m.model_dump(by_alias=True))
#> {'banana': 3.14, 'foo_alias': 'hello', 'bar': {'whatever': (1, 2)}}
请注意,whatever
的值被转储为元组(tuple),这不是一个已知的 JSON 类型。可以将 mode
参数设置为 'json'
以确保使用与 JSON 兼容的类型。
print(m.model_dump(mode='json'))
#> {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': [1, 2]}}
另见
TypeAdapter.dump_python()
方法,在处理非 Pydantic 模型时很有用。
JSON 模式¶
Pydantic 允许将数据直接序列化为 JSON 编码的字符串,它会尽力将 Python 值转换为有效的 JSON 数据。这可以通过使用 model_dump_json()
方法实现。
from datetime import datetime
from pydantic import BaseModel
class BarModel(BaseModel):
whatever: tuple[int, ...]
class FooBarModel(BaseModel):
foo: datetime
bar: BarModel
m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': (1, 2)})
print(m.model_dump_json(indent=2))
"""
{
"foo": "2032-06-01T12:13:14",
"bar": {
"whatever": [
1,
2
]
}
}
"""
除了标准库 json
模块支持的类型外,Pydantic 还支持多种类型(日期和时间类型、UUID
对象、集合等)。如果使用了不支持的类型且无法序列化为 JSON,则会引发 PydanticSerializationError
异常。
另见
TypeAdapter.dump_json()
方法,在处理非 Pydantic 模型时很有用。
遍历模型¶
Pydantic 模型也可以被遍历,产生 (field_name, field_value)
对。请注意,字段值保持原样,因此子模型将不会被转换为字典。
from pydantic import BaseModel
class BarModel(BaseModel):
whatever: int
class FooBarModel(BaseModel):
banana: float
foo: str
bar: BarModel
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})
for name, value in m:
print(f'{name}: {value}')
#> banana: 3.14
#> foo: hello
#> bar: whatever=123
这意味着可以在模型上调用 dict()
来构建模型的字典。
print(dict(m))
#> {'banana': 3.14, 'foo': 'hello', 'bar': BarModel(whatever=123)}
注意
根模型确实会转换为一个键为 'root'
的字典。
Pickle 支持¶
Pydantic 模型支持高效的 pickle 和 unpickle 操作。
import pickle
from pydantic import BaseModel
class FooBarModel(BaseModel):
a: str
b: int
m = FooBarModel(a='hello', b=123)
print(m)
#> a='hello' b=123
data = pickle.dumps(m)
print(data[:20])
#> b'\x80\x04\x95\x95\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main_'
m2 = pickle.loads(data)
print(m2)
#> a='hello' b=123
序列化器¶
与自定义验证器类似,您可以在字段和模型级别利用自定义序列化器来进一步控制序列化行为。
警告
每个字段/模型只能定义一个序列化器。无法将多个序列化器组合在一起(包括 plain 和 wrap 序列化器)。
字段序列化器¶
API 文档
pydantic.functional_serializers.PlainSerializer
pydantic.functional_serializers.WrapSerializer
pydantic.functional_serializers.field_serializer
在其最简单的形式中,字段序列化器是一个可调用对象,它接受要序列化的值作为参数,并返回序列化后的值。
如果向序列化器提供了 return_type
参数(或者序列化器函数上有返回类型注解),它将被用来构建一个额外的序列化器,以确保序列化后的字段值符合此返回类型。
可以使用两种不同类型的序列化器。它们都可以使用注解模式或使用@field_serializer
装饰器来定义,后者可应用于实例方法或静态方法。
-
Plain 序列化器:无条件地被调用以序列化字段。Pydantic 支持的类型的序列化逻辑将不会被调用。使用这类序列化器对于为任意类型指定逻辑也很有用。
from typing import Annotated, Any from pydantic import BaseModel, PlainSerializer def ser_number(value: Any) -> Any: if isinstance(value, int): return value * 2 else: return value class Model(BaseModel): number: Annotated[int, PlainSerializer(ser_number)] print(Model(number=4).model_dump()) #> {'number': 8} m = Model(number=1) m.number = 'invalid' print(m.model_dump()) # (1)! #> {'number': 'invalid'}
- Pydantic 将不会验证序列化后的值是否符合
int
类型。
from typing import Any from pydantic import BaseModel, field_serializer class Model(BaseModel): number: int @field_serializer('number', mode='plain') # (1)! def ser_number(self, value: Any) -> Any: if isinstance(value, int): return value * 2 else: return value print(Model(number=4).model_dump()) #> {'number': 8} m = Model(number=1) m.number = 'invalid' print(m.model_dump()) # (2)! #> {'number': 'invalid'}
'plain'
是装饰器的默认模式,可以省略。- Pydantic 将不会验证序列化后的值是否符合
int
类型。
- Pydantic 将不会验证序列化后的值是否符合
-
Wrap 序列化器:提供更多灵活性来定制序列化行为。您可以在 Pydantic 序列化逻辑之前或之后运行代码。
这类序列化器必须定义一个强制的额外 handler 参数:一个接受要序列化的值作为参数的可调用对象。在内部,此 handler 会将值的序列化委托给 Pydantic。您可以自由选择完全不调用此 handler。
from typing import Annotated, Any from pydantic import BaseModel, SerializerFunctionWrapHandler, WrapSerializer def ser_number(value: Any, handler: SerializerFunctionWrapHandler) -> int: return handler(value) + 1 class Model(BaseModel): number: Annotated[int, WrapSerializer(ser_number)] print(Model(number=4).model_dump()) #> {'number': 5}
from typing import Any from pydantic import BaseModel, SerializerFunctionWrapHandler, field_serializer class Model(BaseModel): number: int @field_serializer('number', mode='wrap') def ser_number( self, value: Any, handler: SerializerFunctionWrapHandler ) -> int: return handler(value) + 1 print(Model(number=4).model_dump()) #> {'number': 5}
该使用哪种序列化器模式¶
虽然两种方法都可以实现相同的目标,但每种模式都提供了不同的好处。
使用注解模式¶
使用注解模式的一个关键好处是使序列化器可重用。
from typing import Annotated
from pydantic import BaseModel, Field, PlainSerializer
DoubleNumber = Annotated[int, PlainSerializer(lambda v: v * 2)]
class Model1(BaseModel):
my_number: DoubleNumber
class Model2(BaseModel):
other_number: Annotated[DoubleNumber, Field(description='My other number')]
class Model3(BaseModel):
list_of_even_numbers: list[DoubleNumber] # (1)!
- 如注解模式文档中所述,我们还可以对注解的特定部分使用序列化器(在这种情况下,序列化应用于列表项,而不是整个列表)。
通过查看字段注解,也更容易理解哪些序列化器应用于某个类型。
使用装饰器模式¶
使用@field_serializer
装饰器的一个关键好处是将函数应用于多个字段。
from pydantic import BaseModel, field_serializer
class Model(BaseModel):
f1: str
f2: str
@field_serializer('f1', 'f2', mode='plain')
def capitalize(self, value: str) -> str:
return value.capitalize()
以下是关于装饰器用法的一些额外说明:
- 如果您希望序列化器应用于所有字段(包括在子类中定义的字段),您可以将
'*'
作为字段名参数传递。 - 默认情况下,装饰器将确保提供的字段名在模型上已定义。如果您想在类创建期间禁用此检查,可以通过将
False
传递给check_fields
参数来实现。当字段序列化器定义在基类上,并且字段预期存在于子类上时,这非常有用。
模型序列化器¶
也可以使用 @model_serializer
装饰器在整个模型上定制序列化。
如果向 @model_serializer
装饰器提供了 return_type
参数(或者序列化器函数上有返回类型注解),它将被用来构建一个额外的序列化器,以确保序列化后的模型值符合此返回类型。
与字段序列化器一样,可以使用两种不同类型的模型序列化器。
-
Plain 序列化器:无条件地被调用以序列化模型。
from pydantic import BaseModel, model_serializer class UserModel(BaseModel): username: str password: str @model_serializer(mode='plain') # (1)! def serialize_model(self) -> str: # (2)! return f'{self.username} - {self.password}' print(UserModel(username='foo', password='bar').model_dump()) #> foo - bar
'plain'
是装饰器的默认模式,可以省略。- 您可以自由返回一个非字典的值。
-
Wrap 序列化器:提供更多灵活性来定制序列化行为。您可以在 Pydantic 序列化逻辑之前或之后运行代码。
这类序列化器必须定义一个强制的额外 handler 参数:一个接受模型实例作为参数的可调用对象。在内部,此 handler 会将模型的序列化委托给 Pydantic。您可以自由选择完全不调用此 handler。
from pydantic import BaseModel, SerializerFunctionWrapHandler, model_serializer class UserModel(BaseModel): username: str password: str @model_serializer(mode='wrap') def serialize_model( self, handler: SerializerFunctionWrapHandler ) -> dict[str, object]: serialized = handler(self) serialized['fields'] = list(serialized) return serialized print(UserModel(username='foo', password='bar').model_dump()) #> {'username': 'foo', 'password': 'bar', 'fields': ['username', 'password']}
序列化信息¶
字段和模型序列化器的可调用对象(在所有模式下)都可以选择性地接受一个额外的 info
参数,提供有用的额外信息,例如:
- 用户定义的上下文
- 当前的序列化模式:
'python'
或'json'
(参见mode
属性) - 在序列化期间使用序列化方法设置的各种参数(例如
exclude_unset
、serialize_as_any
) - 如果使用字段序列化器,则为当前字段的名称(参见
field_name
属性)。
序列化上下文¶
您可以将一个上下文对象传递给序列化方法,然后在序列化器函数内部使用 context
属性访问它。
from pydantic import BaseModel, FieldSerializationInfo, field_serializer
class Model(BaseModel):
text: str
@field_serializer('text', mode='plain')
@classmethod
def remove_stopwords(cls, v: str, info: FieldSerializationInfo) -> str:
if isinstance(info.context, dict):
stopwords = info.context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v
model = Model(text='This is an example document')
print(model.model_dump()) # no context
#> {'text': 'This is an example document'}
print(model.model_dump(context={'stopwords': ['this', 'is', 'an']}))
#> {'text': 'example document'}
同样地,您可以为验证使用上下文。
序列化子类¶
受支持类型的子类¶
受支持类型的子类会根据其父类进行序列化。
from datetime import date
from pydantic import BaseModel
class MyDate(date):
@property
def my_date_format(self) -> str:
return self.strftime('%d/%m/%Y')
class FooModel(BaseModel):
date: date
m = FooModel(date=MyDate(2023, 1, 1))
print(m.model_dump_json())
#> {"date":"2023-01-01"}
类模型类型的子类¶
当使用类模型类型(Pydantic 模型、数据类等)作为字段注解时,默认行为是像序列化该类的实例一样序列化字段值,即使它是一个子类。更具体地说,只有在类型注解上声明的字段才会包含在序列化结果中。
from pydantic import BaseModel
class User(BaseModel):
name: str
class UserLogin(User):
password: str
class OuterModel(BaseModel):
user: User
user = UserLogin(name='pydantic', password='hunter2')
m = OuterModel(user=user)
print(m)
#> user=UserLogin(name='pydantic', password='hunter2')
print(m.model_dump()) # (1)!
#> {'user': {'name': 'pydantic'}}
- 注意:password 字段未被包含
迁移警告
此行为与 Pydantic V1 的工作方式不同,V1 在递归序列化模型到字典时总是会包含所有(子类的)字段。这种行为改变的动机是,它有助于确保您精确地知道在序列化时哪些字段可能被包含,即使在实例化对象时传递了子类。特别是,这有助于在将敏感信息(如密钥)添加为子类字段时避免意外情况。要启用旧的 V1 行为,请参阅下一节。
使用鸭子类型进行序列化 🦆¶
鸭子类型序列化是指根据实际的字段值而不是字段定义来序列化模型实例的行为。这意味着对于一个用类模型类型注解的字段,该类子类中存在的所有字段都将包含在序列化输出中。
此行为可以在字段级别和运行时为特定的序列化调用进行配置:
- 字段级别:使用
SerializeAsAny
注解。 - 运行时级别:调用序列化方法时使用
serialize_as_any
参数。
下面我们更详细地讨论这些选项。
SerializeAsAny
注解¶
如果您想要鸭子类型序列化行为,可以使用 SerializeAsAny
注解在类型上实现。
from pydantic import BaseModel, SerializeAsAny
class User(BaseModel):
name: str
class UserLogin(User):
password: str
class OuterModel(BaseModel):
as_any: SerializeAsAny[User]
as_user: User
user = UserLogin(name='pydantic', password='password')
print(OuterModel(as_any=user, as_user=user).model_dump())
"""
{
'as_any': {'name': 'pydantic', 'password': 'password'},
'as_user': {'name': 'pydantic'},
}
"""
当一个类型被注解为 SerializeAsAny[<type>]
时,其验证行为将与被注解为 <type>
时相同,静态类型检查器也会将该注解视为简单的 <type>
。在序列化时,该字段将被序列化,就好像该字段的类型提示是 Any
一样,这也是这个名字的由来。
serialize_as_any
运行时设置¶
serialize_as_any
运行时设置可用于带有或不带有鸭子类型序列化行为来序列化模型数据。serialize_as_any
可以作为关键字参数传递给各种序列化方法(例如 Pydantic 模型上的 model_dump()
和 model_dump_json()
)。
from pydantic import BaseModel
class User(BaseModel):
name: str
class UserLogin(User):
password: str
class OuterModel(BaseModel):
user1: User
user2: User
user = UserLogin(name='pydantic', password='password')
outer_model = OuterModel(user1=user, user2=user)
print(outer_model.model_dump(serialize_as_any=True)) # (1)!
"""
{
'user1': {'name': 'pydantic', 'password': 'password'},
'user2': {'name': 'pydantic', 'password': 'password'},
}
"""
print(outer_model.model_dump(serialize_as_any=False)) # (2)!
#> {'user1': {'name': 'pydantic'}, 'user2': {'name': 'pydantic'}}
- 当
serialize_as_any
设置为True
时,结果与 V1 的结果匹配。 - 当
serialize_as_any
设置为False
(V2 默认值)时,存在于子类但不存在于基类上的字段不会被包含在序列化中。
字段的包含与排除¶
对于序列化,字段的包含和排除可以通过两种方式配置:
- 在字段级别,使用
Field()
函数上的exclude
和exclude_if
参数。 - 在序列化方法上使用各种序列化参数。
在字段级别¶
在字段级别,可以使用 exclude
和 exclude_if
参数。
from pydantic import BaseModel, Field
class Transaction(BaseModel):
id: int
private_id: int = Field(exclude=True)
value: int = Field(ge=0, exclude_if=lambda v: v == 0)
print(Transaction(id=1, private_id=2, value=0).model_dump())
#> {'id': 1}
字段级别的排除优先于下面描述的 include
序列化参数。
作为序列化方法的参数¶
当使用序列化方法(如 model_dump()
)时,可以使用几个参数来排除或包含字段。
排除和包含特定字段¶
考虑以下模型:
from pydantic import BaseModel, Field, SecretStr
class User(BaseModel):
id: int
username: str
password: SecretStr
class Transaction(BaseModel):
id: str
private_id: str = Field(exclude=True)
user: User
value: int
t = Transaction(
id='1234567890',
private_id='123',
user=User(id=42, username='JohnDoe', password='hashedpassword'),
value=9876543210,
)
exclude
参数可用于指定应排除哪些字段(包含其他字段),反之亦然,使用 include
参数。
# using a set:
print(t.model_dump(exclude={'user', 'value'}))
#> {'id': '1234567890'}
# using a dictionary:
print(t.model_dump(exclude={'user': {'username', 'password'}, 'value': True}))
#> {'id': '1234567890', 'user': {'id': 42}}
# same configuration using `include`:
print(t.model_dump(include={'id': True, 'user': {'id'}}))
#> {'id': '1234567890', 'user': {'id': 42}}
请注意,不支持使用 False
在 exclude
中包含字段(或在 include
中排除字段)。
也可以从序列和字典中排除或包含特定项:
from pydantic import BaseModel
class Hobby(BaseModel):
name: str
info: str
class User(BaseModel):
hobbies: list[Hobby]
user = User(
hobbies=[
Hobby(name='Programming', info='Writing code and stuff'),
Hobby(name='Gaming', info='Hell Yeah!!!'),
],
)
print(user.model_dump(exclude={'hobbies': {-1: {'info'}}})) # (1)!
"""
{
'hobbies': [
{'name': 'Programming', 'info': 'Writing code and stuff'},
{'name': 'Gaming'},
]
}
"""
-
使用
include
的等效调用将是:user.model_dump( include={'hobbies': {0: True, -1: {'name'}}} )
特殊键 '__all__'
可用于将排除/包含模式应用于所有成员。
print(user.model_dump(exclude={'hobbies': {'__all__': {'info'}}}))
#> {'hobbies': [{'name': 'Programming'}, {'name': 'Gaming'}]}
根据字段值排除和包含字段¶
当使用序列化方法时,可以根据字段的值使用以下参数排除字段:
exclude_defaults
:排除所有值与默认值相等(使用相等性(==
)比较运算符)的字段。exclude_none
:排除所有值为None
的字段。-
exclude_unset
:Pydantic 会跟踪在实例化期间被明确设置的字段(使用model_fields_set
属性)。使用exclude_unset
,任何未明确提供的字段都将被排除。from pydantic import BaseModel class UserModel(BaseModel): name: str age: int = 18 user = UserModel(name='John') print(user.model_fields_set) #> {'name'} print(user.model_dump(exclude_unset=True)) #> {'name': 'John'}
请注意,在实例创建后更改字段会将其从未设置字段中移除。
user.age = 21 print(user.model_dump(exclude_unset=True)) #> {'name': 'John', 'age': 21}
提示
实验性的
MISSING
哨兵值可以作为exclude_unset
的替代方案。任何值为MISSING
的字段都会自动从序列化输出中排除。