跳转到内容

序列化

除了通过字段名称(例如 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() 方法实现。

  1. 除了根模型外,根模型会直接转储其根值。
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

序列化器

自定义验证器类似,您可以在字段和模型级别利用自定义序列化器来进一步控制序列化行为。

警告

每个字段/模型只能定义一个序列化器。无法将多个序列化器组合在一起(包括 plainwrap 序列化器)。

字段序列化器

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'}
    
    1. 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'}
    
    1. 'plain' 是装饰器的默认模式,可以省略。
    2. Pydantic 将不会验证序列化后的值是否符合 int 类型。
  • 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)!
  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 参数来实现。当字段序列化器定义在基类上,并且字段预期存在于子类上时,这非常有用。

模型序列化器

API 文档

pydantic.functional_serializers.model_serializer

也可以使用 @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
    
    1. 'plain' 是装饰器的默认模式,可以省略。
    2. 您可以自由返回一个字典的值。
  • 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 参数,提供有用的额外信息,例如:

序列化上下文

您可以将一个上下文对象传递给序列化方法,然后在序列化器函数内部使用 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'}}
  1. 注意:password 字段未被包含

迁移警告

此行为与 Pydantic V1 的工作方式不同,V1 在递归序列化模型到字典时总是会包含所有(子类的)字段。这种行为改变的动机是,它有助于确保您精确地知道在序列化时哪些字段可能被包含,即使在实例化对象时传递了子类。特别是,这有助于在将敏感信息(如密钥)添加为子类字段时避免意外情况。要启用旧的 V1 行为,请参阅下一节。

使用鸭子类型进行序列化 🦆

鸭子类型序列化是指根据实际的字段值而不是字段定义来序列化模型实例的行为。这意味着对于一个用类模型类型注解的字段,该类子类中存在的所有字段都将包含在序列化输出中。

此行为可以在字段级别和运行时为特定的序列化调用进行配置:

下面我们更详细地讨论这些选项。

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'}}
  1. serialize_as_any 设置为 True 时,结果与 V1 的结果匹配。
  2. serialize_as_any 设置为 False(V2 默认值)时,存在于子类但不存在于基类上的字段不会被包含在序列化中。

字段的包含与排除

对于序列化,字段的包含和排除可以通过两种方式配置:

在字段级别

在字段级别,可以使用 excludeexclude_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}}

请注意,不支持使用 Falseexclude包含字段(或在 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'},
    ]
}
"""
  1. 使用 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 的字段都会自动从序列化输出中排除。