使用Pydantic的`parent`属性来验证子项?

huangapple go评论81阅读模式
英文:

Using Pydantic parent attribute to validate child?

问题

class Item(BaseModel):
    value: int

    @validator("value")
    @classmethod
    def validate_value(cls, value, values):
        multiplier = values.get("multiplier", 1)
        return value * multiplier
英文:

Is it possible to use a containing object's attribute during the validation of a child object in a pydantic model?

Given the json data:

# example.json

{
    "multiplier": 5,
    "field_1": {
        "value": 1
    },
    "field_2": {
        "value": 2
    }
}

and the corresponding Pydantic model:

# example.py

from pydantic import BaseModel, validator

class Item(BaseModel):
    value: int

class Container(BaseModel):
    multiplier: int
    field_1: Item
    field_2: Item

is it possible to use the Container object's multiplier attribute during validation of the Item values? For instance, I'd like to do something like this to Item at runtime:

class Item(BaseModel):
    value: int

    @validator("value")
    @classmethod
    def validate_value(cls, value):
        return value # * multiplier  # <--- can I get access to Container's multiplier here?

but I cannot determine if it possible to get access to the Container.multiplier value in a case like this?

In my actual use case, the nesting is much, much deeper and so I would prefer not have the validator up at the Container level as access becomes fairly complicated, but I also do not want to duplicate the multiplier value down at the Item level? Is there any way to pass parameters up and down the object hierarchy within a model of this sort?

答案1

得分: 1

The requested code has been translated.

英文:

Simplest approach is to perform validation on the parent instead of the child:

from pydantic import BaseModel, validator

class Item(BaseModel):
    value: int

class Container(BaseModel):
    multiplier: int
    field_1: Item
    field_2: Item


    @validator("field_1", "field_2")
    def validate_value(cls, v, values):
        """Validate each item"""
        m = values["multiplier"]

If you want to define the validator on the child, you can create a function and then call the validation function from the parent:

class Item(BaseModel):
    value: int

    @classmethod
    def validate_value(cls, v, **kwargs):
        """Validate each item"""
        m = kwargs.get("multiplier")

        if m * v.value < 10:
            raise ValueError

        return v


class Container(BaseModel):
    multiplier: int
    field_1: Item
    field_2: Item

    @validator("field_1", "field_2", pre=False)
    def validate_items(cls, v, values):
        return Item.validate_value(v, **values)

Another alternative is to pass the multiplier as a private model attribute to the children, then the children can use the pydantic validation function, but you'll still need to assign dynamically to the children:

from pydantic import BaseModel, Field, validator


class Item(BaseModel):
    multiplier: int  # exclude from parent serialization, workaround for validation
    value: int

    @validator("value", pre=False)
    def validate_value(cls, v, values):
        """Validate each item"""
        m = values["multiplier"]

        if m * v < 10:
            raise ValueError

        return v


class Container(BaseModel):
    multiplier: int
    field_1: Item = Field(..., exclude={'multiplier'})
    field_2: Item = Field(..., exclude={'multiplier'})

    @validator("field_1", "field_2", pre=True)
    def validate_items(cls, v, values):
        
        # Construct from a value, another workaround
        if isinstance(v, int):
            return Item(value=v, multiplier=values["multiplier"])
        
        elif isinstance(v, Item):
            return Item(value=v.value, multiplier=values["multiplier"])


if __name__ == '__main__':

    c = Container(
        multiplier=1,
        field_1=11,
        field_2=22
    )

答案2

得分: 1

Pydantic 2,结合 Python 的 contextvars 库,提供了一个良好且清晰的解决方案。使用 Pydantic 包装模型验证器,您可以在开始验证子项之前设置上下文变量,然后在验证后清理上下文变量。示例:

from pydantic import BaseModel, field_validator, model_validator
from contextvars import ContextVar

context_multiplier = ContextVar("context_multiplier")

class Item(BaseModel):
    value: int
    
    @field_validator("value")
    @classmethod
    def validate_value(cls, value):
        multiplier = context_multiplier.get()
        if value % multiplier != 0:
            raise ValueError(f"not a multiple of {multiplier}")
        return value

class Container(BaseModel):
    multiplier: int
    field_1: Item
    field_2: Item
    
    @model_validator(mode="wrap")
    @classmethod
    def validate_model(cls, v, handler):
        try:
            multiplier = int(v["multiplier"])
        except KeyError:
            raise ValueError("multiplier required")
        token = context_multiplier.set(v["multiplier"])
        try:
            return handler(v)
        finally:
            context_multiplier.reset(token)

使用正确的输入进行验证成功:

>>> Container.model_validate(
...     {"multiplier": 3, "field_1": {"value": 9}, "field_2": {"value": 12}}
... )
Container(multiplier=3, field_1=Item(value=9), field_2=Item(value=12))

验证会检查输入与乘数的关系:

>>> Container.model_validate(
...     {"multiplier": 3, "field_1": {"value": 9}, "field_2": {"value": 11}}
... )
pydantic_core._pydantic_core.ValidationError: 1 validation error for Container
field_2.value
  Value error, not a multiple of 3 [type=value_error, input_value=11, input_type=int]
    For further information visit https://errors.pydantic.dev/2.1.2/v/value_error

使用 ContextVar 意味着这个解决方案也应该适用于异步代码。

英文:

Pydantic 2, combined with Python's contextvars library, provides a good and clean solution. Using a Pydantic wrap model validator, you can set a context variable before starting validation of the children, then clean up the context variable after validation. Example:


from pydantic import BaseModel, field_validator, model_validator
from contextvars import ContextVar

context_multiplier = ContextVar("context_multiplier")


class Item(BaseModel):
    value: int
    @field_validator("value")
    @classmethod
    def validate_value(cls, value):
        multiplier = context_multiplier.get()
        if value % multiplier != 0:
            raise ValueError(f"not a multiple of {multiplier}")
        return value


class Container(BaseModel):
    multiplier: int
    field_1: Item
    field_2: Item
    @model_validator(mode="wrap")
    @classmethod
    def validate_model(cls, v, handler):
        try:
            multiplier = int(v["multiplier"])
        except KeyError:
            raise ValueError("multiplier required")
        token = context_multiplier.set(v["multiplier"])
        try:
            return handler(v)
        finally:
            context_multiplier.reset(token)

Validation succeeds with correct inputs:

>>> Container.model_validate(
...     {"multiplier": 3, "field_1": {"value": 9}, "field_2": {"value": 12}}
... )
Container(multiplier=3, field_1=Item(value=9), field_2=Item(value=12))

Validation checks the inputs against the multiplier:

>>> Container.model_validate(
...     {"multiplier": 3, "field_1": {"value": 9}, "field_2": {"value": 11}}
... )
pydantic_core._pydantic_core.ValidationError: 1 validation error for Container
field_2.value
  Value error, not a multiple of 3 [type=value_error, input_value=11, input_type=int]
    For further information visit https://errors.pydantic.dev/2.1.2/v/value_error

Using ContextVar means this solution should also work with async code.

huangapple
  • 本文由 发表于 2023年4月19日 23:26:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76056289.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定