使用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 对象的第一个参数是该字段的默认值。如果你不想要任何默认值,你应该把它设置为None 。Field 中的其他关键字参数是模式中的可选属性。
结论
Pydantic的构建方式为灵活性留出了空间。你可以将Pydantic与任何开发框架一起使用,而且工作得很好。
像FastAPI这样的框架支持Pydantic开箱即用。其他松散耦合的框架,比如Flask,并没有与Pydantic捆绑在一起,但是允许有整合的空间。
从文章中的例子来看,Pydantic使你能够控制输入类型的自定义验证,因为输入验证是确保你的应用程序安全的一个重要步骤。