英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论