简体   繁体   English

如何根据 Fastapi Pydantic 中的特定枚举成员进行验证 model

[英]How to validate based on specific Enum member in a Fastapi Pydantic model

Here is my Pydantic model:这是我的 Pydantic model:

from enum import Enum
from pydantic import BaseModel


class ProfileField(str, Enum):
    mobile = "mobile"
    email = "email"
    address = "address"


class ProfileType(str, Enum):
    primary = "primary"
    secondary = "secondary"


class ProfileDetail(BaseModel):
    name: ProfileField
    value: str
    type: ProfileType

My API is accepting this type of JSON and its working fine.我的 API 正在接受这种类型的 JSON 并且工作正常。

{
    "data": [
        {
            "name": "email",
            "value": "abcd@gmail.com",
            "type": "primary"
        }
    ]
}

The requirement is email is string type and needs a regex, mobile is integer type and also needs a regex, and address is a string and needs to be restricted to 50 characters.要求是email是string类型需要regex, mobile是integer类型也需要regex, address是string需要限制在50个字符以内。

Is it possible to add corresponding validations?是否可以添加相应的验证?

Discriminated union and built-in types/validators区分联合和内置类型/验证器

If I understand correctly, the actual JSON data you receive has the top-level data key and its value is an array of objects that you currently represent with your ProfileDetail schema.如果我理解正确,您收到的实际 JSON 数据具有顶级data键,其值是您当前使用ProfileDetail模式表示的对象数组。

If that is the case, you may be better served by not using an Enum at all for your name field and instead defining a discriminated union based on the value of the name field.如果是这种情况,您可能会更好地为您的name字段完全不使用Enum ,而是根据name字段的值定义一个可区分的联合 You can write a separate model for each case ( mobile , email , and address ) and delegate validation to each of them for their own case.您可以为每个案例( mobileemailaddress )编写一个单独的 model ,并将验证委托给每个案例。

Since all three of them share a base schema, you can define a base model for them to inherit from to reduce repetition.由于它们三个共享一个基础架构,因此您可以定义一个基础 model 供它们继承以减少重复。 The type field for example can stay an Enum (Pydantic handles validation of those out of the box ) and can be inherited by the three submodels.例如, type字段可以保留为Enum (Pydantic 处理开箱即用的验证)并且可以由三个子模型继承。

For mobile and address it sounds like you can just use constr to define your constraints via the regex and max_length parameters respectively.对于mobileaddress ,听起来您可以使用constr分别通过regexmax_length参数来定义约束。

For email , you can use the built-in Pydantic type EmailStr (subtype of str ).对于email ,您可以使用内置的 Pydantic 类型EmailStrstr的子类型)。 You'll just need to install the optional dependency with pip install 'pydantic[email]' .您只需要使用pip install 'pydantic[email]'安装可选的依赖项。

That way you should not even need to write any custom validators.这样你甚至不需要编写任何自定义验证器。

Here is the setup I suggest:这是我建议的设置:

from enum import Enum
from typing import Annotated, Literal, Union
from pydantic import BaseModel, EmailStr, Field, constr

class ProfileType(str, Enum):
    primary = "primary"
    secondary = "secondary"

class BaseProfileFieldData(BaseModel):
    value: str
    type: ProfileType

class MobileData(BaseProfileFieldData):
    value: constr(regex=r"\d{5,}")  # your actual regex here
    name: Literal["mobile"]

class EmailData(BaseProfileFieldData):
    value: EmailStr
    name: Literal["email"]

class AddressData(BaseProfileFieldData):
    value: constr(max_length=50)
    name: Literal["address"]

ProfileField = Annotated[
    Union[MobileData, EmailData, AddressData],
    Field(discriminator="name")
]

class ProfileDetails(BaseModel):
    data: list[ProfileField]

Tests测试

Let's test it with some fixtures:让我们用一些固定装置来测试它:

test_data_mobile_valid = {
  "name": "mobile",
  "value": "123456",
  "type": "secondary",
}
test_data_mobile_invalid = {
  "name": "mobile",
  "value": "12",
  "type": "secondary",
}
test_data_email_valid = {
  "name": "email",
  "value": "abcd@gmail.com",
  "type": "primary",
}
test_data_email_invalid = {
  "name": "email",
  "value": "abcd@gmail@..",
  "type": "primary",
}
test_data_address_valid = {
  "name": "address",
  "value": "some street 42, 12345 example",
  "type": "secondary",
}
test_data_address_invalid = {
  "name": "address",
  "value": "x" * 51,
  "type": "secondary",
}
test_data_invalid_name = {
  "name": "foo",
  "value": "x",
  "type": "primary",
}
test_data_invalid_type = {
  "name": "mobile",
  "value": "123456",
  "type": "bar",
}

The first six should be self explanatory.前六个应该是不言自明的。 test_data_invalid_name should cause an error because "foo" is not a valid discriminator value for name . test_data_invalid_name应该会导致错误,因为"foo"不是name的有效鉴别器值。 test_data_invalid_type should demonstrate the built-in enum validator catching the invalid type value "bar" . test_data_invalid_type应该演示捕获无效type"bar"的内置枚举验证器。

Let's test the valid data first:我们先测试有效数据:

if __name__ == "__main__":
    from pydantic import ValidationError

    obj = ProfileDetails.parse_obj({
        "data": [
            test_data_mobile_valid,
            test_data_email_valid,
            test_data_address_valid,
        ]
    })
    print(obj.json(indent=4))
    ...

Output: Output:

{
    "data": [
        {
            "value": "123456",
            "type": "secondary",
            "name": "mobile"
        },
        {
            "value": "abcd@gmail.com",
            "type": "primary",
            "name": "email"
        },
        {
            "value": "some street 42, 12345 example",
            "type": "secondary",
            "name": "address"
        }
    ]
}

No surprises here.这里没有惊喜。 Now test those that should not pass the value validation:现在测试那些不应该通过value验证的:

if __name__ == "__main__":
    ...
    try:
        ProfileDetails.parse_obj({
            "data": [
                test_data_mobile_invalid,
                test_data_email_invalid,
                test_data_address_invalid,
            ]
        })
    except ValidationError as exc:
        print(exc.json(indent=4))
    ...

Output: Output:

[
    {
        "loc": [
            "data",
            0,
            "MobileData",
            "value"
        ],
        "msg": "string does not match regex \"\\d{5,}\"",
        "type": "value_error.str.regex",
        "ctx": {
            "pattern": "\\d{5,}"
        }
    },
    {
        "loc": [
            "data",
            1,
            "EmailData",
            "value"
        ],
        "msg": "value is not a valid email address",
        "type": "value_error.email"
    },
    {
        "loc": [
            "data",
            2,
            "AddressData",
            "value"
        ],
        "msg": "ensure this value has at most 50 characters",
        "type": "value_error.any_str.max_length",
        "ctx": {
            "limit_value": 50
        }
    }
]

Caught all the wrong values.捕获了所有错误的值。 Now just to be sure, the last two fixtures:现在可以肯定的是,最后两个固定装置:

if __name__ == "__main__":
    ...
    try:
        ProfileDetails.parse_obj({
            "data": [
                test_data_invalid_name,
                test_data_invalid_type,
            ]
        })
    except ValidationError as exc:
        print(exc.json(indent=4))

Output: Output:

[
    {
        "loc": [
            "data",
            0
        ],
        "msg": "No match for discriminator 'name' and value 'foo' (allowed values: 'mobile', 'email', 'address')",
        "type": "value_error.discriminated_union.invalid_discriminator",
        "ctx": {
            "discriminator_key": "name",
            "discriminator_value": "foo",
            "allowed_values": "'mobile', 'email', 'address'"
        }
    },
    {
        "loc": [
            "data",
            1,
            "MobileData",
            "type"
        ],
        "msg": "value is not a valid enumeration member; permitted: 'primary', 'secondary'",
        "type": "type_error.enum",
        "ctx": {
            "enum_values": [
                "primary",
                "secondary"
            ]
        }
    }
]

Seems like we get the desired behavior from our model.似乎我们从 model 获得了所需的行为。


Caveat警告

If you really want a separate model like the ProfileDetail you showed in your question, that will not be possible with discriminated unions because those rely on being defined for a field on a separate model. In that case you'll actually have to write a custom validator (probably a root_validator ) to ensure consistency between name and value .如果您真的想要一个单独的 model,就像您在问题中显示的ProfileDetail一样,这对于受歧视的联合来说是不可能的,因为它们依赖于为单独的 model 上的字段定义。在这种情况下,您实际上必须编写自定义验证器(可能是root_validator )以确保namevalue之间的一致性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何在 FastAPI 中使用带有表单数据的 Pydantic model? - How to use a Pydantic model with Form data in FastAPI? pydantic 模型返回 empy (fastapi) - pydantic model returns empy (fastapi) 如何使用字段别名而不是 FastAPI 中的名称返回 Pydantic 模型? - How to return Pydantic model using Field aliases instead of names in FastAPI? 如何在 pydantic model for fastapi 中允许 List 作为查询参数而不是 requestbody - How to allow List as query params instead of requestbody in pydantic model for fastapi 如何使用 FastAPI 从 Pydantic model 中排除可选的未设置值? - How to exclude Optional unset values from a Pydantic model using FastAPI? 使用 Pydantic model 的 FastAPI 查询参数 - FastAPI query parameter using Pydantic model Python / pydantic / FastAPI - 将此数据结构描述为模式模型? - Python / pydantic / FastAPI - describe this datastructure as a Schema Model? 在 pydantic model 中初始化 Literal 枚举 - Initialize a Literal enum in a pydantic model 处理特定的个性化异常/条件(FastAPI、Pydantic 模型、预测 Model 部署) - Handling specific personalised exceptions / conditions ( FastAPI , Pydantic Models , Prediction Model deployment ) 在 FastAPI 中使用 Pydantic 模型进行基于模型的预测时出现错误“value is not a valid dict” - Getting error "value is not a valid dict" when using Pydantic models in FastAPI for model-based predictions
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM