跳到内容

实验性功能

在本节中,您将找到关于 Pydantic 中新的实验性功能的文档。这些功能可能会更改或删除,我们正在征求反馈和建议,以便将它们永久纳入 Pydantic。

有关实验性功能的更多信息,请参阅我们的版本策略

反馈

我们欢迎对实验性功能提供反馈!请在 Pydantic GitHub 仓库 上开启一个 issue,分享您的想法、请求或建议。

我们也鼓励您阅读现有的反馈,并将您的想法添加到现有的 issue 中。

导入时的警告

当您从 experimental 模块导入实验性功能时,您会看到一条警告消息,指出该功能是实验性的。您可以使用以下方法禁用此警告

import warnings

from pydantic import PydanticExperimentalWarning

warnings.filterwarnings('ignore', category=PydanticExperimentalWarning)

Pipeline API

Pydantic v2.8.0 引入了一个实验性的 "pipeline" API,它允许以比现有 API 更类型安全的方式组合解析(验证)、约束和转换。此 API 可能会更改或删除,我们正在征求反馈和建议,以便将其永久纳入 Pydantic。

API 文档

pydantic.experimental.pipeline

通常,pipeline API 用于定义应用于传入数据的验证步骤序列。pipeline API 旨在比现有的 Pydantic API 更类型安全且更具可组合性。

pipeline 中的每个步骤可以是

  • 对提供的类型运行 pydantic 验证的验证步骤
  • 修改数据的转换步骤
  • 检查数据是否符合条件的约束步骤
  • 检查数据是否符合条件,并在返回 False 时引发错误的谓词步骤

请注意,以下示例试图详尽无遗,但代价是复杂性:如果您发现自己在类型注解中编写了如此多的转换,您可能需要考虑使用 UserInUserOut 模型(如下例所示)或类似的模型,在其中您通过惯用的纯 Python 代码进行转换。这些 API 旨在用于代码节省显着且增加的复杂性相对较小的情况。

from __future__ import annotations

from datetime import datetime
from typing import Annotated

from pydantic import BaseModel
from pydantic.experimental.pipeline import validate_as


class User(BaseModel):
    name: Annotated[str, validate_as(str).str_lower()]  # (1)!
    age: Annotated[int, validate_as(int).gt(0)]  # (2)!
    username: Annotated[str, validate_as(str).str_pattern(r'[a-z]+')]  # (3)!
    password: Annotated[
        str,
        validate_as(str)
        .transform(str.lower)
        .predicate(lambda x: x != 'password'),  # (4)!
    ]
    favorite_number: Annotated[  # (5)!
        int,
        (validate_as(int) | validate_as(str).str_strip().validate_as(int)).gt(
            0
        ),
    ]
    friends: Annotated[list[User], validate_as(...).len(0, 100)]  # (6)!
    bio: Annotated[
        datetime,
        validate_as(int)
        .transform(lambda x: x / 1_000_000)
        .validate_as(...),  # (8)!
    ]
  1. 将字符串转换为小写。
  2. 约束整数大于零。
  3. 约束字符串匹配正则表达式模式。
  4. 您还可以使用较低级别的 transform、constrain 和 predicate 方法。
  5. 使用 |& 运算符组合步骤(如逻辑 OR 或 AND)。
  6. 使用 Ellipsis... 作为第一个位置参数调用 validate_as(...) 意味着 validate_as(<字段类型>)。使用 validate_as(Any) 接受任何类型。
  7. 您可以在其他步骤之前或之后调用 validate_as() 以进行预处理或后处理。

BeforeValidatorAfterValidatorWrapValidator 的映射

validate_as 方法是定义 BeforeValidatorAfterValidatorWrapValidator 的更类型安全的方式

from typing import Annotated

from pydantic.experimental.pipeline import transform, validate_as

# BeforeValidator
Annotated[int, validate_as(str).str_strip().validate_as(...)]  # (1)!
# AfterValidator
Annotated[int, transform(lambda x: x * 2)]  # (2)!
# WrapValidator
Annotated[
    int,
    validate_as(str)
    .str_strip()
    .validate_as(...)
    .transform(lambda x: x * 2),  # (3)!
]
  1. 在将字符串解析为整数之前,去除字符串中的空格。
  2. 在将整数解析后,将其乘以 2。
  3. 从字符串中去除空格,将其验证为整数,然后将其乘以 2。

替代模式

根据不同的场景,可以使用许多替代模式。例如,考虑上面提到的 UserInUserOut 模式

from __future__ import annotations

from pydantic import BaseModel


class UserIn(BaseModel):
    favorite_number: int | str


class UserOut(BaseModel):
    favorite_number: int


def my_api(user: UserIn) -> UserOut:
    favorite_number = user.favorite_number
    if isinstance(favorite_number, str):
        favorite_number = int(user.favorite_number.strip())

    return UserOut(favorite_number=favorite_number)


assert my_api(UserIn(favorite_number=' 1 ')).favorite_number == 1

此示例使用纯粹的惯用 Python 代码,可能比上面的示例更易于理解、类型检查等。您选择的方法应真正取决于您的用例。您将需要比较冗长程度、性能、向用户返回有意义的错误难易程度等,以选择正确的模式。请注意不要仅仅因为可以就滥用像 pipeline API 这样的高级模式。

部分验证

Pydantic v2.10.0 引入了对 "部分验证" 的实验性支持。

这允许您验证不完整的 JSON 字符串,或表示不完整输入数据的 Python 对象。

当处理 LLM 的输出时,部分验证特别有用,其中模型流式传输结构化响应,并且您可能希望在仍在接收数据时开始验证流(例如,向用户显示部分数据)。

警告

部分验证是一项实验性功能,在未来的 Pydantic 版本中可能会发生更改。当前的实现应被视为此时的概念验证,并且存在许多局限性

当在 TypeAdapter 上使用三种验证方法时,可以启用部分验证:TypeAdapter.validate_json()TypeAdapter.validate_python()TypeAdapter.validate_strings()。这允许您解析和验证不完整的 JSON,还可以验证通过解析任何格式的不完整数据而创建的 Python 对象。

可以将 experimental_allow_partial 标志传递给这些方法以启用部分验证。它可以采用以下值(默认值为 False

  • False'off' - 禁用部分验证
  • True'on' - 启用部分验证,但不支持尾随字符串
  • 'trailing-strings' - 启用部分验证并支持尾随字符串

'trailing-strings' 模式

'trailing-strings' 模式允许将部分 JSON 末尾的尾随不完整字符串包含在输出中。例如,如果您正在针对以下模型进行验证

from typing import TypedDict


class Model(TypedDict):
    a: str
    b: str

那么,尽管末尾有不完整的字符串,以下 JSON 输入将被视为有效

'{"a": "hello", "b": "wor'

并将被验证为

{'a': 'hello', 'b': 'wor'}

experiment_allow_partial 的作用

from typing import Annotated

from annotated_types import MinLen
from typing_extensions import NotRequired, TypedDict

from pydantic import TypeAdapter


class Foobar(TypedDict):  # (1)!
    a: int
    b: NotRequired[float]
    c: NotRequired[Annotated[str, MinLen(5)]]


ta = TypeAdapter(list[Foobar])

v = ta.validate_json('[{"a": 1, "b"', experimental_allow_partial=True)  # (2)!
print(v)
#> [{'a': 1}]

v = ta.validate_json(
    '[{"a": 1, "b": 1.0, "c": "abcd', experimental_allow_partial=True  # (3)!
)
print(v)
#> [{'a': 1, 'b': 1.0}]

v = ta.validate_json(
    '[{"b": 1.0, "c": "abcde"', experimental_allow_partial=True  # (4)!
)
print(v)
#> []

v = ta.validate_json(
    '[{"a": 1, "b": 1.0, "c": "abcde"},{"a": ', experimental_allow_partial=True
)
print(v)
#> [{'a': 1, 'b': 1.0, 'c': 'abcde'}]

v = ta.validate_python([{'a': 1}], experimental_allow_partial=True)  # (5)!
print(v)
#> [{'a': 1}]

v = ta.validate_python(
    [{'a': 1, 'b': 1.0, 'c': 'abcd'}], experimental_allow_partial=True  # (6)!
)
print(v)
#> [{'a': 1, 'b': 1.0}]

v = ta.validate_json(
    '[{"a": 1, "b": 1.0, "c": "abcdefg',
    experimental_allow_partial='trailing-strings',  # (7)!
)
print(v)
#> [{'a': 1, 'b': 1.0, 'c': 'abcdefg'}]
  1. TypedDict Foobar 有三个字段,但只有 a 是必需的,这意味着即使缺少 bc 字段,也可以创建 Foobar 的有效实例。
  2. 解析 JSON,输入在字符串被截断的点之前是有效的 JSON。
  3. 在这种情况下,输入的截断意味着 c 的值 (abcd) 作为 c 字段的输入无效,因此被省略。
  4. a 字段是必需的,因此对列表中唯一项的验证失败并被删除。
  5. 部分验证也适用于 Python 对象,它应该具有与 JSON 相同的语义,当然,除了您不能拥有真正 "不完整" 的 Python 对象之外。
  6. 与上面相同,但使用 Python 对象,c 被删除,因为它不是必需的并且验证失败。
  7. trailing-strings 模式允许将部分 JSON 末尾的不完整字符串包含在输出中,在这种情况下,输入在字符串被截断的点之前是有效的 JSON,因此包含最后一个字符串。

部分验证的工作原理

部分验证遵循 Pydantic 的禅宗思想 — 它不对输入数据可能是什么做出保证,但它保证返回您所需类型的有效实例,或引发验证错误。

为此,experimental_allow_partial 标志启用了两种行为

1. 部分 JSON 解析

Pydantic 使用的 jiter JSON 解析器已经支持解析部分 JSON,experimental_allow_partial 只是通过 allow_partial 参数传递给 jiter。

注意

如果您只想进行纯 JSON 解析,并支持部分 JSON,您可以直接使用 jiter Python 库,或者在调用 pydantic_core.from_json 时传递 allow_partial 参数。

2. 忽略输入中最后一个元素的错误

仅访问部分输入数据意味着错误通常可能发生在输入数据的最后一个元素中。

例如

  • 如果字符串具有约束 MinLen(5),当您只看到部分输入时,验证可能会失败,因为字符串的一部分丢失了(例如,{"name": "Sam 而不是 {"name": "Samuel"}
  • 如果 int 字段具有约束 Ge(10),当您只看到部分输入时,验证可能会失败,因为数字太小了(例如,1 而不是 10
  • 如果 TypedDict 字段有 3 个必需字段,但部分输入只有其中两个字段,则验证将失败,因为缺少某些字段
  • 等等,等等 — 还有更多类似的情况

关键是,如果您只看到某些有效输入数据的一部分,则验证错误通常可能发生在序列的最后一个元素或映射的最后一个值中。

为了避免这些错误破坏部分验证,Pydantic 将忽略输入数据最后一个元素中的所有错误。

最后一个元素中的错误被忽略
from typing import Annotated

from annotated_types import MinLen

from pydantic import BaseModel, TypeAdapter


class MyModel(BaseModel):
    a: int
    b: Annotated[str, MinLen(5)]


ta = TypeAdapter(list[MyModel])
v = ta.validate_json(
    '[{"a": 1, "b": "12345"}, {"a": 1,',
    experimental_allow_partial=True,
)
print(v)
#> [MyModel(a=1, b='12345')]

部分验证的局限性

仅限 TypeAdapter

您只能将 experiment_allow_partial 传递给 TypeAdapter 方法,它尚不支持通过其他 Pydantic 入口点,例如 BaseModel

支持的类型

目前,只有一部分集合验证器知道如何处理部分验证

  • list
  • set
  • frozenset
  • dict(如 dict[X, Y] 中所示)
  • TypedDict — 只有非必需字段可能缺失,例如,通过 NotRequiredtotal=False

虽然您可以在针对包含其他集合验证器的类型进行验证时使用 experimental_allow_partial,但这些类型将被 "完全或全无" 地验证,并且部分验证将不适用于更嵌套的类型。

例如,在上面的示例中,尽管列表中的第二个项目被完全删除,但部分验证仍然有效,因为 BaseModel (尚) 不支持部分验证。

但是,部分验证在以下示例中根本不起作用,因为 BaseModel 不支持部分验证,因此它不会将 allow_partial 指令转发到 b 中的列表验证器

from typing import Annotated

from annotated_types import MinLen

from pydantic import BaseModel, TypeAdapter, ValidationError


class MyModel(BaseModel):
    a: int = 1
    b: list[Annotated[str, MinLen(5)]] = []  # (1)!


ta = TypeAdapter(MyModel)
try:
    v = ta.validate_json(
        '{"a": 1, "b": ["12345", "12', experimental_allow_partial=True
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for MyModel
    b.1
      String should have at least 5 characters [type=string_too_short, input_value='12', input_type=str]
    """
  1. b 的列表验证器没有获得模型验证器传递给它的 allow_partial 指令,因此它不知道忽略输入中最后一个元素的错误。

一些无效但完整的 JSON 将被接受

jiter(Pydantic 使用的 JSON 解析器)的工作方式意味着目前无法区分完整的 JSON(如 {"a": 1, "b": "12"})和不完整的 JSON(如 {"a": 1, "b": "12)。

这意味着在使用 experimental_allow_partial 时,Pydantic 将接受一些无效的 JSON,例如

from typing import Annotated

from annotated_types import MinLen
from typing_extensions import TypedDict

from pydantic import TypeAdapter


class Foobar(TypedDict, total=False):
    a: int
    b: Annotated[str, MinLen(5)]


ta = TypeAdapter(Foobar)

v = ta.validate_json(
    '{"a": 1, "b": "12', experimental_allow_partial=True  # (1)!
)
print(v)
#> {'a': 1}

v = ta.validate_json(
    '{"a": 1, "b": "12"}', experimental_allow_partial=True  # (2)!
)
print(v)
#> {'a': 1}
  1. 尽管最后一个字段由于验证失败而被省略,但这将按预期通过验证。
  2. 由于传递给 pydantic-core 的 JSON 数据的二进制表示形式与前一种情况无法区分,因此这也将通过验证。
from typing import Annotated

from annotated_types import MinLen
from typing import TypedDict

from pydantic import TypeAdapter


class Foobar(TypedDict, total=False):
    a: int
    b: Annotated[str, MinLen(5)]


ta = TypeAdapter(Foobar)

v = ta.validate_json(
    '{"a": 1, "b": "12', experimental_allow_partial=True  # (1)!
)
print(v)
#> {'a': 1}

v = ta.validate_json(
    '{"a": 1, "b": "12"}', experimental_allow_partial=True  # (2)!
)
print(v)
#> {'a': 1}
  1. 尽管最后一个字段由于验证失败而被省略,但这将按预期通过验证。
  2. 由于传递给 pydantic-core 的 JSON 数据的二进制表示形式与前一种情况无法区分,因此这也将通过验证。

输入中最后一个字段的任何错误都将被忽略

上面所述,许多错误可能是由截断输入引起的。Pydantic 没有试图专门忽略可能由截断引起的错误,而是在部分验证模式下忽略输入最后一个元素中的所有错误。

这意味着,如果错误发生在输入的最后一个字段中,则明显无效的数据将通过验证

from typing import Annotated

from annotated_types import Ge

from pydantic import TypeAdapter

ta = TypeAdapter(list[Annotated[int, Ge(10)]])
v = ta.validate_python([20, 30, 4], experimental_allow_partial=True)  # (1)!
print(v)
#> [20, 30]

ta = TypeAdapter(list[int])

v = ta.validate_python([1, 2, 'wrong'], experimental_allow_partial=True)  # (2)!
print(v)
#> [1, 2]
  1. 正如您所期望的那样,这将通过验证,因为 Pydantic 正确地忽略了(截断的)最后一个项目中的错误。
  2. 由于忽略了最后一个项目中的错误,这也将通过验证。

可调用对象参数的验证

Pydantic 提供了 @validate_call 装饰器,用于对可调用对象的提供的参数(以及返回值类型)执行验证。但是,它只允许通过实际调用装饰的可调用对象来提供参数。在某些情况下,您可能只想验证参数,例如从 JSON 数据等其他数据源加载时。

因此,实验性的 generate_arguments_schema() 函数可用于构造核心模式,该模式稍后可以与 SchemaValidator 一起使用。

from pydantic_core import SchemaValidator

from pydantic.experimental.arguments_schema import generate_arguments_schema


def func(p: bool, *args: str, **kwargs: int) -> None: ...


arguments_schema = generate_arguments_schema(func=func)

val = SchemaValidator(arguments_schema, config={'coerce_numbers_to_str': True})

args, kwargs = val.validate_json(
    '{"p": true, "args": ["arg1", 1], "kwargs": {"extra": 1}}'
)
print(args, kwargs)  # (1)!
#> (True, 'arg1', '1') {'extra': 1}
  1. 如果您想要将验证后的参数作为字典,可以使用 Signature.bind() 方法

    from inspect import signature
    
    signature(func).bind(*args, **kwargs).arguments
    #> {'p': True, 'args': ('arg1', '1'), 'kwargs': {'extra': 1}}
    

注意

@validate_call 不同,此核心模式仅验证提供的参数,而不会调用底层的可调用对象。

这个新的核心模式将成为 Pydantic V3 中 @validate_call 使用的默认模式。

此外,您可以通过提供回调来忽略特定参数,该回调为每个参数调用

from typing import Any

from pydantic_core import SchemaValidator

from pydantic.experimental.arguments_schema import generate_arguments_schema


def func(p: bool, *args: str, **kwargs: int) -> None: ...


def skip_first_parameter(index: int, name: str, annotation: Any) -> Any:
    if index == 0:
        return 'skip'


arguments_schema = generate_arguments_schema(
    func=func,
    parameters_callback=skip_first_parameter,
)

val = SchemaValidator(arguments_schema)

args, kwargs = val.validate_json('{"args": ["arg1"], "kwargs": {"extra": 1}}')
print(args, kwargs)
#> ('arg1',) {'extra': 1}