如何用Pydantic进行严格的类型验证

1,265 阅读6分钟

使用Pydantic进行严格的类型验证

类型验证是确保你得到的东西是你所期望的东西的过程。如果一个端点应该得到一个整数,你就用类型验证来确保输入的是一个整数而不是一个字符串。编写你的验证逻辑可能很费时。

许多API框架有开箱即用的类型验证,而像Flask这样的轻量级编程框架则没有。通过像Pydantic这样的框架,类型验证可以变得更容易获得。

在这篇文章中,我们将看看Pydantic的各种功能以及如何使用它们的例子。

前提条件

为了让读者能够继续阅读,他们必须具备以下条件。

  • 安装有Python(>=3.6)。
  • 安装有Pydantic
pip install pydantic

数据模型

Pydantic中的对象是用模型来定义的。一个模型类继承自BaseModel 类。所有的字段和自定义验证逻辑都位于数据模型类中。

一个简单的例子是,一个定义了用户配置文件以及它所包含的字段的模型。

from pydantic import BaseModel

class Profile(BaseModel):
    firstname: str
    lastname: str
    location: str
    bio: str

在上面的片段中,数据模型的名称是Profile. 它继承了Pydantic的BaseModel 类;它还定义了配置文件中的一些字段,如firstname,lastname, 以及它们的类型。在这种情况下,它们都被期望为字符串。

# create a new profile with some fields
new_profile = {
    "firstname": "Tomi",
    "lastname": "Bamimore",
}

# validate the new_profile with the Profile data model
profile = Profile(**new_profile)
print(profile)

将包含新配置文件信息的new_profile 字典传递给Profile 模型,将验证new_profile

如果你运行上面的代码片段,你会得到这个错误。

pydantic.error_wrappers.ValidationError: 2 validation errors for Profile
location
 field required (type=value_error.missing)
bio
 field required (type=value_error.missing)

new_profile 字典中缺少的字段导致了这个错误。Pydantic使数据模型中定义的所有字段都默认为 "必填"。

或者,你可以使用Python标准库中的typing 模块所定义的Optional ,使一个字段成为可选项。

from typing import Optional
from pydantic import BaseModel

# create a pydantic data model
class Profile(BaseModel):
    firstname: str
    lastname: str
    location: Optional[str]
    bio: Optional[str]

# create a new profile with some fields
new_profile = {
    "firstname": "Tomi",
    "lastname": "Bamimore",
}

# validate the new_profile with the Profile data model
profile = Profile(**new_profile)
print(profile.json())

输出。

{"firstname": "Tomi", "lastname": "Bamimore", "location": null, "bio": null}

这一次,输出是一个JSON。

当使用API时,JSON输出很有用。你也可以像Python中对象的属性一样访问结果。

new_profile = {
    "firstname": "Jane",
    "lastname": "Doe",
}
profile = Profile(**new_profile)
print(profile.firstname, profile.lastname)

输出。

Jane Doe

递归模型

在处理嵌套字段时,出现了将一个数据模型作为另一个模型的数据类型的情况。

一个数据模型可以被声明为另一个数据模型中的一个类型。当一个模型中的一个字段有其他与之相关的子字段时,你需要递归模型。这就是递归模型的概念。

在下面的例子中,Bio 被定义为一个数据模型。Bio 也是Profile 模型中的一个类型。

from typing import Optional

from pydantic import BaseModel

class Bio(BaseModel):
    age: Optional[int]
    profession: str
    school: str

class Profile(BaseModel):
    firstname: str
    lastname: str
    location: Optional[str]
    # Model Bio is now the type of a field in the Profile model
    bio: Bio

new_profile = {
    "firstname": "Jane",
    "lastname": "Doe",
    "bio": {"age": 38, "profession": "Nurse", "school": "MIT"},
}
profile = Profile(**new_profile)
print(profile.dict())

输出。

{
    "firstname": "Jane",
    "lastname": "Doe",
    "location": None,
    "bio": {"age": 38, "profession": "Nurse", "school": "MIT"},
}

Pydantic的字段类型

Pydantic支持来自Python标准库的广泛的字段类型。这个列表是无限的,在本文中无法穷尽。

Pydantic也有自定义的类型,比如PaymentCardNumber

在下面的片段中,可以看到它是如何工作的。

from pydantic import BaseModel

from pydantic.types import PaymentCardNumber, ConstrainedInt

# Define the Payment model
class Payment(BaseModel):
    # card_number is defined as a field with PaymentCardNumber type
    card_number: PaymentCardNumber

# A valid credit card number
new_card = 4238721116652766
new_payment = Payment(card_number=new_card)
print(new_payment)

输出。

card_number='4238721116652766'

信用卡号码使用Luhn算法进行验证。Pydantic在引擎盖下运行验证,以验证对card_number 字段的任何输入。

如果输入是无效的。

from pydantic import BaseModel, conint
from pydantic.types import PaymentCardNumber

class Payment(BaseModel):
    # constrain amount to greater than or equal to 300
    amount: conint(ge=300)
    card_number: PaymentCardNumber

new_card = 7618972848548894
new_payment = Payment(card_number=new_card, amount=300)
print(new_payment)

输出:信用卡号码是使用Luhn算法进行验证的。

pydantic.error_wrappers.ValidationError: 1 validation error for Payment
card_number
 card number is not luhn valid (type=value_error.payment_card_number.luhn_check)

自定义验证器

Pydantic还允许编写自定义验证方法。在处理需要自定义验证的通用数据类型时,它是非常有用的。

在下面的例子中,我们将验证一个雇员ID。它是一个包含四个整数、一个连字符和两个字母的字符串。

例如,2345-HG

from pydantic import BaseModel, validator

class Employee(BaseModel):
    employee_id: str
    # validator decorator is used to wrap custom validation fuction for a field
    @validator("employee_id")
    def employee_id_validator(cls, value):
        splitted = value.split("-")
        if len(splitted) != 2:
            raise ValueError("Invalid id")
        if len(splitted[0]) != 4:
            raise ValueError("Invalid id")
        if len(splitted[1]) != 2:
            raise ValueError("Invalid id")
        return value

# validate a new employee id
new_employee = Employee(employee_id="234-HG")
print(new_employee)

输出。

pydantic.error_wrappers.ValidationError: 1 validation error for Employee
employee_id
 Invalid id (type=value_error)

在上面的代码片断中,一个不正确的employee_id ,被传入模型。Pydantic运行自定义验证器,如果有任何检查失败,则返回一个错误。

如果输入正确,它就会成功运行。

new_employee = Employee(employee_id="2345-HG")
print(new_employee.dict())

输出。

{'employee_id': '2345-HG'}

你的验证逻辑可以像你想要的那样复杂。在处理Pydantic模型时,使用try/except块是一个很好的做法。

try:
    new_employee = Employee(employee_id="2345-HG")
    print(new_employee.dict())

except:
    # Error handling logic
    print("ERROR")

生成JSON模式

Pydantic模型可以生成具有OpenAPI规范的JSON模式投诉。你可以使用Field 对象来填充模式中的信息。

模式有助于定义一个JSON文档的结构。模式对于生成API文档是需要的。

from typing import Optional
from pydantic import BaseModel, Field

class Bio(BaseModel):
    age: Optional[int]
    profession: str
    school: str

class Profile(BaseModel):
    firstname: str = Field("Jane", title="Firstname", description="User's firstname")
    lastname: str = Field("Doe", title="Lastname", description="User's lastname")
    location: Optional[str] = Field(
        None, title="Location", description="User's location"
    )
    bio: Optional[Bio] = Field(None, title="Bio", description="Short bio of user")

# create a new profile
new_profile = {"firstname": "Tomi", "lastname": "Bamimore"}
profile = Profile(**new_profile)
# generate json schema
print(profile.schema_json())

输出。

{
    "title": "Profile",
    "type": "object",
    "properties": {
        "firstname": {
            "title": "Firstname",
            "description": "User's firstname",
            "default": "Jane",
            "type": "string",
        },
        "lastname": {
            "title": "Lastname",
            "description": "User's lastname",
            "default": "Doe",
            "type": "string",
        },
        "location": {
            "title": "Location",
            "description": "User's location",
            "type": "string",
        },
        "bio": {
            "title": "Bio",
            "description": "Short bio of user",
            "allOf": [{"$ref": "#/definitions/Bio"}],
        },
    },
    "definitions": {
        "Bio": {
            "title": "Bio",
            "type": "object",
            "properties": {
                "age": {"title": "Age", "type": "integer"},
                "profession": {"title": "Profession", "type": "string"},
                "school": {"title": "School", "type": "string"},
            },
            "required": ["profession", "school"],
        }
    },
}

Field 对象的第一个参数是该字段的默认值。如果你不想要任何默认值,你应该把它设置为NoneField 中的其他关键字参数是模式中的可选属性。

结论

Pydantic的构建方式为灵活性留出了空间。你可以将Pydantic与任何开发框架一起使用,而且工作得很好。

像FastAPI这样的框架支持Pydantic开箱即用。其他松散耦合的框架,比如Flask,并没有与Pydantic捆绑在一起,但是允许有整合的空间。

从文章中的例子来看,Pydantic使你能够控制输入类型的自定义验证,因为输入验证是确保你的应用程序安全的一个重要步骤。