如何验证 Pydantic 模型的多个字段?

2025-03-04 08:25:00
admin
原创
101
摘要:问题描述:我想验证 Pydantic 模型的三个模型字段。为此,我root_validator从 pydantic 导入,但出现以下错误:from pydantic import BaseModel, ValidationError, root_validator Traceback (most recent...

问题描述:

我想验证 Pydantic 模型的三个模型字段。为此,我root_validator从 pydantic 导入,但出现以下错误:

from pydantic import BaseModel, ValidationError, root_validator
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'root_validator' from 'pydantic' (C:UsersLenovoAppDataLocalProgramsPythonPython38-32libsite-packagespydantic__init__.py)

我尝试过这个:

@validator
def validate_all(cls, v, values, **kwargs):
    ...

我从父模型的一些公共字段继承了我的 pydantic 模型。值仅显示父类字段,但不显示我的子类字段。例如:

class Parent(BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str
    
    @validator
    def validate_all(cls, v, values, **kwargs):
        #here values showing only (name and comment) but not address and phone.
        ...

解决方案 1:

为了扩展的答案Rahul R,此示例更详细地展示了如何使用pydantic验证器。

此示例包含回答您的问题所需的所有信息。

请注意,还有使用 的选项@root_validator,如 所述Kentgrav,请参阅帖子底部的示例以了解更多详细信息。

import pydantic

class Parent(pydantic.BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str

    # If you want to apply the Validator to the fields "name", "comments", "address", "phone"
    @pydantic.validator("name", "comments", "address", "phone")
    @classmethod
    def validate_all_fields_one_by_one(cls, field_value):
        # Do the validation instead of printing
        print(f"{cls}: Field value {field_value}")

        return field_value  # this is the value written to the class field

    # if you want to validate to content of "phone" using the other fields of the Parent and Child class
    @pydantic.validator("phone")
    @classmethod
    def validate_one_field_using_the_others(cls, field_value, values, field, config):
        parent_class_name = values["name"]
        parent_class_address = values["address"] # works because "address" is already validated once we validate "phone"
        # Do the validation instead of printing
        print(f"{field_value} is the {field.name} of {parent_class_name}")

        return field_value 

Customer(name="Peter", comments="Pydantic User", address="Home", phone="117")

输出

<class '__main__.Customer'>: Field value Peter
<class '__main__.Customer'>: Field value Pydantic User
<class '__main__.Customer'>: Field value Home
<class '__main__.Customer'>: Field value 117
117 is the phone number of Peter
Customer(name='Peter', comments='Pydantic User', address='Home', phone='117')

更详细地回答你的问题:

将需要验证的字段@validator直接添加到验证函数上方的装饰器中。

  • @validator("name")`"name"使用(例如)的字段值"Peter"作为验证函数的输入。该类及其父类的所有字段都可以添加到@validator`装饰器中。

  • 然后,验证函数 ( validate_all_fields_one_by_one) 使用字段值作为第二个参数 ( field_value) 来验证输入。验证函数的返回值写入类字段。验证函数的签名是def validate_something(cls, field_value)可以任意选择函数和变量名称的地方(但第一个参数应该是cls)。根据 Arjan(https://youtu.be/Vj-iU-8_xLs?t=329),还@classmethod应该添加装饰器。

如果目标是使用父类和子类的其他(已验证的)字段来验证一个字段,则验证函数的完整签名是def validate_something(cls, field_value, values, field, config)(参数名称valuesfield并且config 必须匹配),其中可以使用字段名称作为键来访问字段的值(例如values["comments"])。

编辑1:如果您只想检查某种类型的输入值,则可以使用以下结构:

@validator("*") # validates all fields
def validate_if_float(cls, value):
    if isinstance(value, float):
        # do validation here
    return value

编辑2:使用以下更简单的方法来验证所有字段@root_validator

import pydantic

class Parent(pydantic.BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str

    @pydantic.root_validator()
    @classmethod
    def validate_all_fields_at_the_same_time(cls, field_values):
        # Do the validation instead of printing
        print(f"{cls}: Field values are: {field_values}")
        assert field_values["name"] != "invalid_name", f"Name `{field_values['name']}` not allowed."
        return field_values

输出

Customer(name="valid_name", comments="", address="Street 7", phone="079")
<class '__main__.Customer'>: Field values are: {'name': 'valid_name', 'comments': '', 'address': 'Street 7', 'phone': '079'}
Customer(name='valid_name', comments='', address='Street 7', phone='079')
Customer(name="invalid_name", comments="", address="Street 7", phone="079")
ValidationError: 1 validation error for Customer
__root__
  Name `invalid_name` not allowed. (type=assertion_error)

解决方案 2:

选项 1 - 使用@validator装饰器

根据文档,“通过向其传递多个字段名称,可以将单个字段validator应用于多个字段”(并且“也可以通过传递特殊值在所有'*'字段上调用”)。因此,您可以将要验证的字段添加到validator装饰器中,并使用field.name属性检查每次validator调用时要验证哪一个。如果某个字段未通过验证,您可以raise ValueError“捕获并用于填充”(请参阅​​此处的ValidationError“注释”部分)。如果您需要根据其他字段验证字段,则必须首先检查它们是否已使用方法进行了验证,如本答案(更新 2)所示。下面演示了一个示例,其中验证了诸如、和数字(基于提供的)之类的字段。提供的正则表达式模式只是本演示的示例,并且基于此和此答案。values.get()`namecountry_codephone`country_code

from pydantic import BaseModel, validator
import re

name_pattern = re.compile(r'[a-zA-Zs]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(+44s?7d{3}|(?07d{3})?)s?d{3}s?d{3}$')  # UK mobile phone number. Valid example: +44 7222 555 555
US_phone_pattern = re.compile(r'^(([0-9]{3}) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$')  # US phone number. Valid example: (123) 123-1234
phone_patterns = {"uk": UK_phone_pattern, "us": US_phone_pattern}

class Parent(BaseModel):
    name: str
    comments: str
    
class Customer(Parent):
    address: str
    country_code: str
    phone: str

    @validator('name', 'country_code', 'phone')
    def validate_atts(cls, v, values, field):
        if field.name == "name":
            if not name_pattern.match(v): raise ValueError(f'{v} is not a valid name.')
        elif field.name == "country_code":
             if not v.lower() in country_codes: raise ValueError(f'{v} is not a valid country code.')
        elif field.name == "phone" and values.get('country_code'):
            c_code = values.get('country_code').lower()
            if not phone_patterns[c_code].match(v): raise ValueError(f'{v} is not a valid phone number.')
        return v

更新 - Pydantic V2 示例

在 Pydantic V2 中,@validator已被弃用,并被 取代。如果你想从 内的另一个字段@field_validator访问,这可以使用,它是字段名称到字段值的字典。values`@field_validator`ValidationInfo.data

from pydantic import BaseModel, ValidationInfo, field_validator
import re

# ... the rest of the code is the same as above


class Customer(Parent):
    address: str
    country_code: str
    phone: str

    @field_validator('name', 'country_code', 'phone')
    @classmethod
    def validate_atts(cls, v: str, info: ValidationInfo):
        if info.field_name == 'name':
            if not name_pattern.match(v): raise ValueError(f'{v} is not a valid name.')
        elif info.field_name == 'country_code':
             if not v.lower() in country_codes: raise ValueError(f'{v} is not a valid country code.')
        elif info.field_name == 'phone' and info.data.get('country_code'):
            c_code = info.data.get('country_code').lower()
            if not phone_patterns[c_code].match(v): raise ValueError(f'{v} is not a valid phone number.')
        return v

选项 2 - 使用@root_validator装饰器

另一种方法是使用@root_validator,它允许对整个模型的数据进行验证。

from pydantic import BaseModel, root_validator
import re

name_pattern = re.compile(r'[a-zA-Zs]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(+44s?7d{3}|(?07d{3})?)s?d{3}s?d{3}$')  # UK mobile phone number. Valid example: +44 7222 555 555
US_phone_pattern = re.compile(r'^(([0-9]{3}) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$')  # US phone number. Valid example: (123) 123-1234
phone_patterns = {"uk": UK_phone_pattern, "us": US_phone_pattern}

class Parent(BaseModel):
    name: str
    comments: str
    
class Customer(Parent):
    address: str
    country_code: str
    phone: str

    @root_validator()
    def validate_atts(cls, values):
        name = values.get('name')
        comments = values.get('comments')
        address = values.get('address')
        country_code = values.get('country_code')
        phone = values.get('phone')
        
        if name is not None and not name_pattern.match(name): 
            raise ValueError(f'{name} is not a valid name.')
        if country_code is not None and not country_code.lower() in country_codes: 
            raise ValueError(f'{country_code} is not a valid country code.')
        if phone is not None and country_code is not None:
            if not phone_patterns[country_code.lower()].match(phone): 
                raise ValueError(f'{phone} is not a valid phone number.')
                
        return values

更新 - Pydantic V2 示例

在 Pydantic V2 中,@root_validator已被弃用,并被 取代@model_validator。模型验证器可以是mode='before'mode='after'mode='wrap'。在这种情况下,mode='after'最适合。如文档中所述:

mode='after'验证器是实例方法,并且始终接收模型实例作为第一个参数。您不应将其用作(cls, ModelType)签名,而应使用(self)并让类型检查器为您推断类型self。由于这些是完全类型安全的,因此它们通常比验证器更容易实现mode='before'
。如果任何字段验证失败,mode='after'则不会调用该字段的验证器。

使用 mode='after'
from pydantic import BaseModel, model_validator
import re

# ... the rest of the code is the same as above


class Customer(Parent):
    address: str
    country_code: str
    phone: str

    @model_validator(mode='after')
    def validate_atts(self):
        name = self.name
        comments = self.comments
        address = self.address
        country_code = self.country_code
        phone = self.phone
        
        if name is not None and not name_pattern.match(name): 
            raise ValueError(f'{name} is not a valid name.')
        if country_code is not None and not country_code.lower() in country_codes: 
            raise ValueError(f'{country_code} is not a valid country code.')
        if phone is not None and country_code is not None:
            if not phone_patterns[country_code.lower()].match(phone): 
                raise ValueError(f'{phone} is not a valid phone number.')
                
        return self
使用 mode='before'

如果您更愿意使用mode='before,您可以按如下方式操作。请注意,在这种情况下,您应该自行检查字段值是否符合预期格式(例如,str在下面的示例中),然后再进行进一步的处理/验证(例如,将值转换为小写、字符串值比较等)——下面未包括。

from pydantic import BaseModel, model_validator
from typing import Any
import re

# ... the rest of the code is the same as above


class Customer(Parent):
    address: str
    country_code: str
    phone: str

    @model_validator(mode='before')
    @classmethod
    def validate_atts(cls, data: Any):
        if isinstance(data, dict):
            name = data.get('name')
            comments = data.get('comments')
            address = data.get('address')
            country_code = data.get('country_code')
            phone = data.get('phone')
            
            if name is not None and not name_pattern.match(name): 
                raise ValueError(f'{name} is not a valid name.')
            if country_code is not None and not country_code.lower() in country_codes: 
                raise ValueError(f'{country_code} is not a valid country code.')
            if phone is not None and country_code is not None:
                if not phone_patterns[country_code.lower()].match(phone): 
                    raise ValueError(f'{phone} is not a valid phone number.')
                
        return data

选项 1 和 2 的测试示例

from pydantic import ValidationError

# should throw "Value error, (123) 123-1234 is not a valid phone number."
try:
    Customer(name='john', comments='hi', address='some address', country_code='UK', phone='(123) 123-1234')
except ValidationError as e:
    print(e)

# should work without errors
print(Customer(name='john', comments='hi', address='some address', country_code='UK', phone='+44 7222 555 555'))

解决方案 3:

您需要将字段作为装饰器的参数传递。

class Parent(BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str

    @validator("name", "coments", "address", "phone")
    def validate_all(cls, v, values, **kwargs):

解决方案 4:

首先,如果您在导入root_validator时遇到错误,我会更新 pydantic。

pip install -U pydantic

上面的许多示例向您展示了如何一次对多个值使用同一个验证器。或者它们增加了很多不必要的复杂性来实现您想要的效果。您只需使用以下代码,即可使用root_validator装饰器在同一验证器中同时验证多个字段。:

from pydantic import root_validator
from pydantic import BaseModel

class Parent(BaseModel):
    name: str = "Peter"
    comments: str = "Pydantic User"

class Customer(Parent):
    address: str = "Home"
    phone: str = "117"

    @root_validator
    def validate_all(cls, values):
         print(f"{values}")
         values["phone"] = "111-111-1111"
         values["address"] = "1111 Pydantic Lane"
         print(f"{values}")
         return values

Output:

{'name': 'Peter', 'comments': 'Pydantic User', 'address': 'Home', 'phone': '117'}

{'name': 'Peter', 'comments': 'Pydantic User', 'address': '1111 Pydantic Lane', 'phone': '111-111-1111'}

解决方案 5:

这里的许多答案都涉及如何将验证器添加到单个 pydantic 模型。我将添加如何在模型之间共享验证器 - 以及其他一些高级技术。

注意:以下内容适用于 Pydantic V2。

选项 1.装饰器中的多个字段validate(...)

先前对这个问题的回答提供了验证多个字段的最简单和最容易的方法 - 即向单个验证器提供多个字段名称:


    ...

    @field_validator("field_1", "field_2", ...)
    def my_validator(...):
        ...

现有的答案已经足够了,我建议阅读它们以进一步了解。

选项 2. 定义单个验证函数并在父级和子级之间重用:

这实际上允许您在整个项目中导入/重用验证器。

from pydantic import field_validator, BaseModel


def must_be_title_case(v: str) -> str:
    """Validator to be used throughout"""
    if v != v.title():
        raise ValueError("must be title cased")
    return v



class Parent(BaseModel):
    name: str = "Peter"
    comments: str = "Pydantic User"
    
    validate_fields = field_validator("name", "comments")(must_be_title_case)


class Customer(Parent):
    address: str = "Home"
    phone: str = "117"
     
    validate_fields = field_validator("address", "phone")(must_be_title_case)

或者,如果您愿意,您可以仅为子项定义所有字段的字段验证:

class Parent(BaseModel):
    name: str = "Peter"
    comments: str = "Pydantic User"


class Customer(Parent):
    address: str = "Home"
    phone: str = "117"
     
    validate_fields = field_validator("name", "comments", "address", "phone")(must_be_title_case)

选项 3. 将您的验证定义为带注释的验证器:

这使您可以定义可重复使用的经过验证的“类型” - 灵活性非常高:

from typing_extensions import Annotated

from pydantic import BaseModel, ValidationError, field_validator
from pydantic.functional_validators import AfterValidator


# Same function as before
def must_be_title_case(v: str) -> str:
    """Validator to be used throughout"""
    if v != v.title():
        raise ValueError("must be title cased")
    return v


# Define your annotated (validated) type:
MySpecialString = Annotated[str, AfterValidator(must_be_title_case)]


# Now use the custom type in your models
class Customer(Parent):
    address: MySpecialString = "Home"
    phone: MySpecialString = "117"

class Parent(BaseModel):
    name: MySpecialString = "Peter"
    comments: MySpecialString = "Pydantic User"

稍微解释一下注释类型中发生的事情:

  • 基类型是string

  • Pydantic 将尝试将输入值强制转换为字符串。这被视为“核心验证”步骤

  • 在 pydantic 的验证之后,我们将运行我们的验证器函数(由 声明AfterValidator)——如果成功,则设置返回的值。

  • 您也可以选择BeforeValidator在注释中声明,它将在 Pydantic 尝试强制值之前运行我们的函数。


选项 4.相互验证字段:

前面的方法展示了如何单独验证多个字段。但是如果你想比较两个值怎么办?

一个常见的例子是比较两个可选的日期值 - 如果两个都设置了,则确保一个大于另一个。我将在下面演示这一点:

比较多个字段的最佳方法是使用model_validator(又名 v1 中的 root_validator):

class MyModel(BaseModel):
    date_1: Optional[datetime] = None
    date_2: Optional[datetime] = None

    @model_validator(mode="after")
    def validate_dates(self):
        """Date 1 must always be larger than date 2, if they are both set"""
        if self.date_1 and self.date_2:
            if self.date_1 < self.date_2:
                raise ValueError("date_2 cannot be larger than date_1")
        return self

注意mode="after"——这允许 pydantic 首先执行其自己的验证(将值强制转换为日期时间对象 + 设置默认值)。

从技术上讲,您可以使用字段验证器执行类似操作,但不能保证在验证时在模型上设置其他字段值 - 请参阅Pydantic 文档中的额外说明。


我希望这能为您设计解决方案提供足够的背景信息。

解决方案 6:

此示例包含回答您的问题所需的所有信息。

    class User(BaseModel):
        name: Optional[str] = ""

        class Config:
            validate_assignment = True

        @validator("name")
            def set_name(cls, name):
            return name or "foo"
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2757  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1693  
  在全球化的浪潮下,企业的业务范围不断拓展,跨文化协作变得愈发普遍。不同文化背景的团队成员在合作过程中,由于语言、价值观、工作习惯等方面的差异,往往会面临诸多沟通挑战。而产品生命周期管理(PLM)系统作为企业管理产品全生命周期的重要工具,如何有效支持跨文化协作成为了关键问题。通过合理运用沟通策略,PLM系统能够在跨文化团...
plm是什么软件   15  
  PLM(产品生命周期管理)系统在企业的产品研发、生产与管理过程中扮演着至关重要的角色,其中文档版本控制是确保产品数据准确性、完整性和可追溯性的关键环节。有效的文档版本控制能够避免因版本混乱导致的错误、重复工作以及沟通不畅等问题,提升企业整体的运营效率和产品质量。接下来,我们将深入探讨 PLM 系统实现文档版本控制的 6...
plm是什么意思   19  
  PLM(产品生命周期管理)项目管理旨在通过有效整合流程、数据和人员,优化产品从概念到退役的整个生命周期。在这个过程中,敏捷测试成为确保产品质量、加速交付的关键环节。敏捷测试强调快速反馈、持续改进以及与开发的紧密协作,对传统的测试流程提出了新的挑战与机遇。通过对测试流程的优化,能够更好地适应PLM项目的动态变化,提升产品...
plm管理系统   18  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用