除了可以直接通过字段名(例如 model.foobar
)访问模型属性之外,还可以通过多种方式转换、转储、序列化和导出模型。
提示
"序列化与转储" Pydantic 中 "序列化" 和 "转储" 可互换使用。两者都指将模型转换为字典或 JSON 编码的字符串的过程。
Outside of Pydantic, the word "serialize" usually refers to converting in-memory data into a string or bytes. However, in the context of Pydantic, there is a very close relationship between converting an object from a more structured form — such as a Pydantic model, a dataclass, etc. — into a less structured form comprised of Python built-ins such as dict.
While we could (and on occasion, do) distinguish between these scenarios by using the word "dump" when converting to primitives and "serialize" when converting to string, for practical purposes, we frequently use the word "serialize" to refer to both of these situations, even though it does not always imply conversion to a string or bytes.
model.model_dump(...)
¶
这是将模型转换为字典的主要方式。子模型将递归地转换为字典。
注意
将子模型转换为字典的一个例外是, RootModel
及其子类将直接转储 root
字段值,而无需包装字典。这也是递归进行的。
注意
您可以使用计算字段将 property
和 cached_property
数据包含在 model.model_dump(...)
输出中。
示例: 翻译文本:
from typing import Any, List, Optional
from pydantic import BaseModel, Field, Json
class BarModel(BaseModel):
whatever: 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': 123})
# returns a dictionary:
print(m.model_dump())
#> {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': 123}}
print(m.model_dump(include={'foo', 'bar'}))
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(m.model_dump(exclude={'foo', 'bar'}))
#> {'banana': 3.14}
print(m.model_dump(by_alias=True))
#> {'banana': 3.14, 'foo_alias': 'hello', 'bar': {'whatever': 123}}
print(
FooBarModel(foo='hello', bar={'whatever': 123}).model_dump(
exclude_unset=True
)
)
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(
FooBarModel(banana=1.1, foo='hello', bar={'whatever': 123}).model_dump(
exclude_defaults=True
)
)
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(
FooBarModel(foo='hello', bar={'whatever': 123}).model_dump(
exclude_defaults=True
)
)
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(
FooBarModel(banana=None, foo='hello', bar={'whatever': 123}).model_dump(
exclude_none=True
)
)
#> {'foo': 'hello', 'bar': {'whatever': 123}}
class Model(BaseModel):
x: List[Json[Any]]
print(Model(x=['{"a": 1}', '[1, 2]']).model_dump())
#> {'x': [{'a': 1}, [1, 2]]}
print(Model(x=['{"a": 1}', '[1, 2]']).model_dump(round_trip=True))
#> {'x': ['{"a":1}', '[1,2]']}
model.model_dump_json(...)
¶
.model_dump_json()
方法将模型直接序列化为一个 JSON 编码的字符串,该字符串等同于 .model_dump()
产生的结果。
有关更多信息,请参见 参数。
注意
Pydantic 可以将许多常用类型序列化为 JSON,否则这些类型与简单的 json.dumps(foobar)
(例如 datetime
、 date
或 UUID
)不兼容。
from datetime import datetime
from pydantic import BaseModel
class BarModel(BaseModel): whatever: int
class FooBarModel(BaseModel): foo: datetime bar: BarModel
m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 123}) print(m.model_dump_json())
> {"foo":"2032-06-01T12:13:14","bar":{"whatever":123}}¶
print(m.model_dump_json(indent=2)) """ { "foo": "2032-06-01T12:13:14", "bar": { "whatever": 123 } } """
dict(model)
和迭代¶
Pydantic 模型也可以使用 dict(model)
转换为字典,并且可以使用 for field_name, field_value in model:
迭代模型的字段。通过这种方法返回原始字段值,因此子模型不会转换为字典。
示例: 翻译文本:
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})
print(dict(m))
#> {'banana': 3.14, 'foo': 'hello', 'bar': BarModel(whatever=123)}
for name, value in m:
print(f'{name}: {value}')
#> banana: 3.14
#> foo: hello
#> bar: whatever=123
还请注意, RootModel
确实会被转换为一个字典,其中的键为 'root'
。
自定义序列化器¶
Pydantic 提供了几个功能序列化器来自定义模型如何序列化为字典或 JSON。
序列化可以使用 @field_serializer
修饰符在字段上进行自定义,也可以使用 @model_serializer
修饰符在模型上进行自定义。
from datetime import datetime, timedelta, timezone
from typing import Any, Dict
from pydantic import BaseModel, ConfigDict, field_serializer, model_serializer
class WithCustomEncoders(BaseModel):
model_config = ConfigDict(ser_json_timedelta='iso8601')
dt: datetime
diff: timedelta
@field_serializer('dt')
def serialize_dt(self, dt: datetime, _info):
return dt.timestamp()
m = WithCustomEncoders(
dt=datetime(2032, 6, 1, tzinfo=timezone.utc), diff=timedelta(hours=100)
)
print(m.model_dump_json())
#> {"dt":1969660800.0,"diff":"P4DT4H"}
class Model(BaseModel):
x: str
@model_serializer
def ser_model(self) -> Dict[str, Any]:
return {'x': f'serialized {self.x}'}
print(Model(x='test value').model_dump_json())
#> {"x":"serialized test value"}
注意
通过将特殊值 '*' 传递给 @field_serializer
装饰器,也可以在所有字段上调用单个序列化程序。
此外, PlainSerializer
和 WrapSerializer
使您能够使用函数修改序列化的输出。
这两个序列化器都接受可选参数,包括:
-
return_type
指定函数的返回类型。如果省略,将从类型注释中推断出来。 -
when_used
指定何时使用此序列化程序。接受一个字符串,其值为'always'、'unless-none'、'json'和'json-unless-none'。默认值为'always'。
PlainSerializer
使用一个简单的函数修改序列化的输出。
from typing_extensions import Annotated
from pydantic import BaseModel
from pydantic.functional_serializers import PlainSerializer
FancyInt = Annotated[
int, PlainSerializer(lambda x: f'{x:,}', return_type=str, when_used='json')
]
class MyModel(BaseModel):
x: FancyInt
print(MyModel(x=1234).model_dump())
#> {'x': 1234}
print(MyModel(x=1234).model_dump(mode='json'))
#> {'x': '1,234'}
WrapSerializer
接收原始输入以及一个处理函数,该处理函数应用标准序列化逻辑,并可以在返回序列化的最终输出之前修改结果值。
from typing import Any
from typing_extensions import Annotated
from pydantic import BaseModel, SerializerFunctionWrapHandler
from pydantic.functional_serializers import WrapSerializer
def ser_wrap(v: Any, nxt: SerializerFunctionWrapHandler) -> str:
return f'{nxt(v + 1):,}'
FancyInt = Annotated[int, WrapSerializer(ser_wrap, when_used='json')]
class MyModel(BaseModel):
x: FancyInt
print(MyModel(x=1234).model_dump())
#> {'x': 1234}
print(MyModel(x=1234).model_dump(mode='json'))
#> {'x': '1,235'}
覆盖转储模型时的返回类型¶
虽然 .model_dump()
的返回值通常可以描述为 dict[str, Any]
,但是通过使用 @model_serializer
,实际上可以导致它返回一个与该签名不匹配的值:
from pydantic import BaseModel, model_serializer
class Model(BaseModel):
x: str
@model_serializer
def ser_model(self) -> str:
return self.x
print(Model(x='not a dict').model_dump())
#> not a dict
如果你想这样做,并且仍然希望对此方法进行正确的类型检查,可以在 if TYPE_CHECKING:
块中重写 .model_dump()
:
from typing import TYPE_CHECKING, Any
from typing_extensions import Literal
from pydantic import BaseModel, model_serializer
class Model(BaseModel):
x: str
@model_serializer
def ser_model(self) -> str:
return self.x
if TYPE_CHECKING:
# Ensure type checkers see the correct return type
def model_dump(
self,
*,
mode: Literal['json', 'python'] | str = 'python',
include: Any = None,
exclude: Any = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
warnings: bool = True,
) -> str: ...
这个技巧实际上在 RootModel
中被用于这个目的。
序列化子类¶
标准类型的子类¶
标准类型的子类会像它们的父类一样自动转储:
from datetime import date, timedelta
from typing import Any, Type
from pydantic_core import core_schema
from pydantic import BaseModel, GetCoreSchemaHandler
class DayThisYear(date):
"""
Contrived example of a special type of date that
takes an int and interprets it as a day in the current year
"""
@classmethod
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(
cls.validate,
core_schema.int_schema(),
serialization=core_schema.format_ser_schema('%Y-%m-%d'),
)
@classmethod
def validate(cls, v: int):
return date(2023, 1, 1) + timedelta(days=v)
class FooModel(BaseModel):
date: DayThisYear
m = FooModel(date=300)
print(m.model_dump_json())
#> {"date":"2023-10-28"}
BaseModel
和 TypedDict
字段的子类实例¶
当使用其注释本身是结构体类型的字段(例如, BaseModel
子类、数据类等)时,默认行为是将属性值序列化为好像它是注释类型的实例一样,即使它是子类。更具体地说,只有注释类型的字段将包含在转储对象中:
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()) # note: the password field is not included
#> {'user': {'name': 'pydantic'}}
警告
"迁移警告" 这种行为与 Pydantic V1 中的工作方式不同,在 Pydantic V1 中,我们总是在递归地将模型转储到字典时包含所有(子类)字段。这种行为改变的动机是为了确保您在序列化时确切知道可以包含哪些字段,即使在实例化对象时传递了子类也是如此。特别是,这有助于防止在添加敏感信息(如秘密)作为子类的字段时出现意外情况。
使用鸭子类型进行序列化 🦆¶
什么是鸭子类型的序列化?
Duck-typing serialization is the behavior of serializing an object based on the fields present in the object itself,
rather than the fields present in the schema of the object. This means that when an object is serialized, fields present in
a subclass, but not in the original schema, will be included in the serialized output.
This behavior was the default in Pydantic V1, but was changed in V2 to help ensure that you know precisely which
fields would be included when serializing, even if subclasses get passed when instantiating the object. This helps
prevent security risks when serializing subclasses with sensitive information, for example.
如果你想要 v1 风格的鸭子类型序列化行为,可以使用运行时设置,也可以注释个别类型。
-
字段/类型级别:使用
SerializeAsAny
注释 -
运行时级别:在调用
model_dump()
或model_dump_json()
时使用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[<SomeType>]
时,验证行为将与注解为 <SomeType>
时相同,类型检查器如 mypy 也将把该属性视为具有适当的类型。但在序列化时,该字段将被序列化为好像该字段的类型提示是 Any
,这就是这个名字的由来。
serialize_as_any
运行时设置¶
serialize_as_any
运行时设置可用于序列化模型数据,也可以使用鸭子类型序列化行为。 serialize_as_any
可以作为关键字参数传递给@s 和@s 的 model_dump()
和 model_dump_json
方法。也可以作为关键字参数传递给@s 的 dump_python()
和 dump_json()
方法。
如果将 serialize_as_any
设置为 True
,则模型将使用鸭子类型序列化行为进行序列化,这意味着模型将忽略模式,而是询问对象本身应该如何进行序列化。特别是,这意味着在序列化模型子类时,将包含子类中存在但原始模式中不存在的字段。
如果将 serialize_as_any
设置为 False
(这是默认值),则模型将使用该模式进行序列化,这意味着子类中存在但原始模式中不存在的字段将被忽略。
问题
“为什么这面旗帜有用?”有时,您希望确保无论子类中可能添加了哪些字段,序列化的对象都将仅具有原始类型定义中列出的字段。如果您在子类中添加了类似于 password: str
的字段,并且您不想意外地将其包含在序列化输出中,那么这可能会很有用。
例如:
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 的默认值)时,子类上存在但父类上不存在的字段不会包含在序列化中。
此设置甚至对嵌套和递归模式也有效。例如:
from typing import List
from pydantic import BaseModel
class User(BaseModel):
name: str
friends: List['User']
class UserLogin(User):
password: str
class OuterModel(BaseModel):
user: User
user = UserLogin(
name='samuel',
password='pydantic-pw',
friends=[UserLogin(name='sebastian', password='fastapi-pw', friends=[])],
)
print(OuterModel(user=user).model_dump(serialize_as_any=True)) # (1)!
"""
{
'user': {
'name': 'samuel',
'friends': [
{'name': 'sebastian', 'friends': [], 'password': 'fastapi-pw'}
],
'password': 'pydantic-pw',
}
}
"""
print(OuterModel(user=user).model_dump(serialize_as_any=False)) # (2)!
"""
{'user': {'name': 'samuel', 'friends': [{'name': 'sebastian', 'friends': []}]}}
"""
-
即使嵌套的
User
模型实例也会使用User
子类特有的字段进行转储。 -
即使嵌套的
User
模型实例也会被转储,而不会包含User
子类特有的字段。
注意
serialize_as_any
运行时标志的行为与 SerializeAsAny
注释的行为几乎相同。有一些细微的差异,我们正在努力解决,但在大多数情况下,您可以期望两者具有相同的行为。有关这些差异的更多信息,请参阅此活跃问题。
覆盖 serialize_as_any
默认值(否)¶
你可以通过配置一个子类来覆盖 serialize_as_any
的默认设置,该子类覆盖了 BaseModel
对 serialize_as_any
参数的默认设置,然后将其用作任何你希望具有此默认行为的模型的基类(而不是 pydantic.BaseModel
)。
例如,如果你想默认使用鸭子类型序列化,可以执行以下操作:
from typing import Any, Dict
from pydantic import BaseModel, SecretStr
class MyBaseModel(BaseModel):
def model_dump(self, **kwargs) -> Dict[str, Any]:
return super().model_dump(serialize_as_any=True, **kwargs)
def model_dump_json(self, **kwargs) -> str:
return super().model_dump_json(serialize_as_any=True, **kwargs)
class User(MyBaseModel):
name: str
class UserInfo(User):
password: SecretStr
class OuterModel(MyBaseModel):
user: User
u = OuterModel(user=UserInfo(name='John', password='secret_pw'))
print(u.model_dump_json()) # (1)!
#> {"user":{"name":"John","password":"**********"}}
-
默认情况下,
model_dump_json
将使用鸭子类型的序列化行为,这意味着password
字段包含在输出中。
pickle.dumps(model)
¶
Pydantic 模型支持高效的序列化和反序列化。
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
高级包含和排除¶
model_dump
和 model_dump_json
方法支持 include
和 exclude
参数,这些参数可以是集合或字典。这允许嵌套选择要导出的字段:
from pydantic import BaseModel, SecretStr
class User(BaseModel):
id: int
username: str
password: SecretStr
class Transaction(BaseModel):
id: str
user: User
value: int
t = Transaction(
id='1234567890',
user=User(id=42, username='JohnDoe', password='hashedpassword'),
value=9876543210,
)
# using a set:
print(t.model_dump(exclude={'user', 'value'}))
#> {'id': '1234567890'}
# using a dict:
print(t.model_dump(exclude={'user': {'username', 'password'}, 'value': True}))
#> {'id': '1234567890', 'user': {'id': 42}}
print(t.model_dump(include={'id': True, 'user': {'id'}}))
#> {'id': '1234567890', 'user': {'id': 42}}
True
表示我们要排除或包含整个键,就好像我们将其包含在一个集合中一样。这可以在任何深度级别进行。
在从子模型或字典的列表或元组中包含或排除字段时必须特别小心。在这种情况下, model_dump
和相关方法期望用于元素级包含或排除的整数键。要从列表或元组的每个成员中排除一个字段,可以使用字典键 '__all__'
,如下所示:
import datetime
from typing import List
from pydantic import BaseModel, SecretStr
class Country(BaseModel):
name: str
phone_code: int
class Address(BaseModel):
post_code: int
country: Country
class CardDetails(BaseModel):
number: SecretStr
expires: datetime.date
class Hobby(BaseModel):
name: str
info: str
class User(BaseModel):
first_name: str
second_name: str
address: Address
card_details: CardDetails
hobbies: List[Hobby]
user = User(
first_name='John',
second_name='Doe',
address=Address(
post_code=123456, country=Country(name='USA', phone_code=1)
),
card_details=CardDetails(
number='4212934504460000', expires=datetime.date(2020, 5, 1)
),
hobbies=[
Hobby(name='Programming', info='Writing code and stuff'),
Hobby(name='Gaming', info='Hell Yeah!!!'),
],
)
exclude_keys = {
'second_name': True,
'address': {'post_code': True, 'country': {'phone_code'}},
'card_details': True,
# You can exclude fields from specific members of a tuple/list by index:
'hobbies': {-1: {'info'}},
}
include_keys = {
'first_name': True,
'address': {'country': {'name'}},
'hobbies': {0: True, -1: {'name'}},
}
# would be the same as user.model_dump(exclude=exclude_keys) in this case:
print(user.model_dump(include=include_keys))
"""
{
'first_name': 'John',
'address': {'country': {'name': 'USA'}},
'hobbies': [
{'name': 'Programming', 'info': 'Writing code and stuff'},
{'name': 'Gaming'},
],
}
"""
# To exclude a field from all members of a nested list or tuple, use "__all__":
print(user.model_dump(exclude={'hobbies': {'__all__': {'info'}}}))
"""
{
'first_name': 'John',
'second_name': 'Doe',
'address': {
'post_code': 123456,
'country': {'name': 'USA', 'phone_code': 1},
},
'card_details': {
'number': SecretStr('**********'),
'expires': datetime.date(2020, 5, 1),
},
'hobbies': [{'name': 'Programming'}, {'name': 'Gaming'}],
}
"""
对于 model_dump_json
方法也是如此。
模型级和字段级包含和排除¶
除了显式参数 exclude
和 include
传递给 model_dump
和 model_dump_json
方法外,我们还可以将 exclude: bool
参数直接传递给 Field
构造函数:
在字段构造函数( Field(..., exclude=True)
)上设置 exclude
优先于 model_dump
和 model_dump_json
上的 exclude
/ include
:
from pydantic import BaseModel, Field, SecretStr
class User(BaseModel):
id: int
username: str
password: SecretStr = Field(..., exclude=True)
class Transaction(BaseModel):
id: str
value: int = Field(exclude=True)
t = Transaction(
id='1234567890',
value=9876543210,
)
print(t.model_dump())
#> {'id': '1234567890'}
print(t.model_dump(include={'id': True, 'value': True})) # (1)!
#> {'id': '1234567890'}
-
由于在
Field
中被排除,因此value
被排除在输出之外。
也就是说,在字段构造函数( Field(..., exclude=True)
)上设置 exclude
并不优先于 model_dump
和 model_dump_json
上的 exclude_unset
、 exclude_none
和 exclude_default
参数:
from typing import Optional
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str
age: Optional[int] = Field(None, exclude=False)
person = Person(name='Jeremy')
print(person.model_dump())
#> {'name': 'Jeremy', 'age': None}
print(person.model_dump(exclude_none=True)) # (1)!
#> {'name': 'Jeremy'}
print(person.model_dump(exclude_unset=True)) # (2)!
#> {'name': 'Jeremy'}
print(person.model_dump(exclude_defaults=True)) # (3)!
#> {'name': 'Jeremy'}
-
由于
exclude_none
设置为True
,并且age
是None
,因此age
被排除在输出之外。 -
由于
exclude_unset
设置为True
,并且在 Person 构造函数中没有设置age
,因此age
被排除在输出之外。 -
由于
exclude_defaults
设置为True
,并且age
采用了None
的默认值,因此age
被排除在输出之外。
序列化上下文¶
你可以将一个上下文对象传递给序列化方法,这些方法可以从装饰后的序列化函数的 info
参数中访问。当你需要在运行时动态更新序列化行为时,这很有用。例如,如果你希望根据动态可控的允许值集转储一个字段,可以通过传递允许的值作为上下文来实现:
from pydantic import BaseModel, SerializationInfo, field_serializer
class Model(BaseModel):
text: str
@field_serializer('text')
def remove_stopwords(self, v: str, info: SerializationInfo):
context = info.context
if context:
stopwords = context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v
model = Model.model_construct(**{'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'}
print(model.model_dump(context={'stopwords': ['document']}))
#> {'text': 'This is an example'}
同样,您也可以使用上下文进行验证。
model_copy(...)
¶
model_copy()
允许复制模型(可选更新),这在处理冻结模型时特别有用。
示例: 翻译文本:
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})
print(m.model_copy(update={'banana': 0}))
#> banana=0 foo='hello' bar=BarModel(whatever=123)
print(id(m.bar) == id(m.model_copy().bar))
#> True
# normal copy gives the same object reference for bar
print(id(m.bar) == id(m.model_copy(deep=True).bar))
#> False
# deep copy gives a new object reference for `bar`
本文总阅读量次