如何让 Pydantic 区分 List[Union[TypeA, TypeB]] 中的字段?

ano*_*spp 12 pydantic

我正在尝试使用 Pydantic 验证 Rest API 的 POST 请求负载。申请人列表可以包含主要申请人和可选的其他申请人。到目前为止,我已经编写了下面列出的 Pydantic 模型,以尝试反映这一点。Rest API json 有效负载使用布尔字段isPrimary来区分主要申请人和其他申请人。

from datetime import date
from pydantic import BaseModel, validator
from typing import List, Literal, Optional, Union


class PrimaryApplicant(BaseModel):
    isPrimary: Literal[True]
    dateOfBirth: Optional[date]


class OtherApplicant(BaseModel):
    isPrimary: Literal[False]
    dateOfBirth: date
    relationshipStatus: Literal["family", "friend", "other", "partner"]


class Application(BaseModel):
    applicants: List[Union[PrimaryApplicant, OtherApplicant]]

    @validator("applicants")
    def validate(
        cls,
        v: List[Union[PrimaryApplicant, OtherApplicant]]
    ) -> List[Union[PrimaryApplicant, OtherApplicant]]:

        list_count = len(v)
        primary_count = len(
            list(
                filter(lambda item: item.isPrimary, v)
            )
        )
        secondary_count = list_count - primary_count

        if primary_count > 1:
            raise ValueError("Only one primary applicant required")

        if secondary_count > 1:
            raise ValueError("Only one secondary applicant allowed")

        return v


def main() -> None:
    data_dict = {
        "applicants": [
            {
                "isPrimary": True
            },
            {
                "isPrimary": False,
                "dateOfBirth": date(1990, 1, 15),
                "relationshipStatus": "family"
            },
        ]
    }

    _ = Application(**data_dict)


if __name__ == "__main__":
    main()

Run Code Online (Sandbox Code Playgroud)

使用上面列出的示例 json 有效负载,当我尝试从有效负载中删除一些必需的强制字段时,会正确引发OtherApplicanta 。ValidationError例如,如果我尝试删除“relationshipStatus”或“dateOfBirth”字段,则会引发错误。然而,isPrimaryPydantic 也报告该字段无效。Pydantic 认为isPrimary字段应该是 True???下面列出了 Pydantic 验证输出示例。

OtherApplicant为什么 Pydantic 期望json 负载中的列表项的isPrimary 字段应该为 True ?PrimaryApplicant由于使用了 ,它是否以某种方式将有效负载与 关联起来Union?如果是这样,我如何让 Pydantic 使用该isPrimary字段来区分列表有效负载中的主要申请人和其他申请人?

其他申请人的列表有效负载中缺少关系状态字段

pydantic.error_wrappers.ValidationError: 2 validation errors for Application
applicants -> 1 -> isPrimary
  unexpected value; permitted: True (type=value_error.const; given=False; permitted=(True,))
applicants -> 1 -> dateOfBirth
  field required (type=value_error.missing)
Run Code Online (Sandbox Code Playgroud)

OtherApplicant 的列表负载中缺少 dateOfBirth 字段

pydantic.error_wrappers.ValidationError: 2 validation errors for Application
applicants -> 1 -> isPrimary
  unexpected value; permitted: True (type=value_error.const; given=False; permitted=(True,))
applicants -> 1 -> relationshipStatus
  field required (type=value_error.missing)
Run Code Online (Sandbox Code Playgroud)

ano*_*spp 22

通过在Pydantic GitHub 存储库上询问也找到了答案

Pydantic 1.9 引入了歧视联盟的概念。

升级到 Pydantic 1.9 并添加:

Applicant = Annotated[
    Union[PrimaryApplicant, OtherApplicant],
    Field(discriminator="isPrimary")]
Run Code Online (Sandbox Code Playgroud)

applicants: List[Applicant]现在我的模型中可以有字段了Application。该isPrimary字段被标记为用于区分主申请人和其他申请人。

因此,完整的代码清单是:

from datetime import date
from pydantic import BaseModel, Field, validator
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated


class PrimaryApplicant(BaseModel):
    isPrimary: Literal[True]
    dateOfBirth: Optional[date]


class OtherApplicant(BaseModel):
    isPrimary: Literal[False]
    dateOfBirth: date
    relationshipStatus: Literal["family", "friend", "other", "partner"]


Applicant = Annotated[
    Union[PrimaryApplicant, OtherApplicant],
    Field(discriminator="isPrimary")]


class Application(BaseModel):
    applicants: List[Applicant]

    @validator("applicants")
    def validate(cls, v: List[Applicant]) -> List[Applicant]:

        list_count = len(v)
        primary_count = len(
            list(
                filter(lambda item: item.isPrimary, v)
            )
        )
        secondary_count = list_count - primary_count

        if primary_count > 1:
            raise ValueError("Only one primary applicant required")

        if secondary_count > 1:
            raise ValueError("Only one secondary applicant allowed")

        return v


def main() -> None:
    data_dict = {
        "applicants": [
            {
                "isPrimary": True
            },
            {
                "isPrimary": False,
                "relationshipStatus": "family"
            },
        ]
    }

    _ = Application(**data_dict)


if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)