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

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

Using Pydantic parent attribute to validate child?

问题

  1. class Item(BaseModel):
  2. value: int
  3. @validator("value")
  4. @classmethod
  5. def validate_value(cls, value, values):
  6. multiplier = values.get("multiplier", 1)
  7. 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:

  1. # example.json
  2. {
  3. "multiplier": 5,
  4. "field_1": {
  5. "value": 1
  6. },
  7. "field_2": {
  8. "value": 2
  9. }
  10. }

and the corresponding Pydantic model:

  1. # example.py
  2. from pydantic import BaseModel, validator
  3. class Item(BaseModel):
  4. value: int
  5. class Container(BaseModel):
  6. multiplier: int
  7. field_1: Item
  8. 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:

  1. class Item(BaseModel):
  2. value: int
  3. @validator("value")
  4. @classmethod
  5. def validate_value(cls, value):
  6. 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:

  1. from pydantic import BaseModel, validator
  2. class Item(BaseModel):
  3. value: int
  4. class Container(BaseModel):
  5. multiplier: int
  6. field_1: Item
  7. field_2: Item
  8. @validator("field_1", "field_2")
  9. def validate_value(cls, v, values):
  10. """Validate each item"""
  11. 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:

  1. class Item(BaseModel):
  2. value: int
  3. @classmethod
  4. def validate_value(cls, v, **kwargs):
  5. """Validate each item"""
  6. m = kwargs.get("multiplier")
  7. if m * v.value < 10:
  8. raise ValueError
  9. return v
  10. class Container(BaseModel):
  11. multiplier: int
  12. field_1: Item
  13. field_2: Item
  14. @validator("field_1", "field_2", pre=False)
  15. def validate_items(cls, v, values):
  16. 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:

  1. from pydantic import BaseModel, Field, validator
  2. class Item(BaseModel):
  3. multiplier: int # exclude from parent serialization, workaround for validation
  4. value: int
  5. @validator("value", pre=False)
  6. def validate_value(cls, v, values):
  7. """Validate each item"""
  8. m = values["multiplier"]
  9. if m * v < 10:
  10. raise ValueError
  11. return v
  12. class Container(BaseModel):
  13. multiplier: int
  14. field_1: Item = Field(..., exclude={'multiplier'})
  15. field_2: Item = Field(..., exclude={'multiplier'})
  16. @validator("field_1", "field_2", pre=True)
  17. def validate_items(cls, v, values):
  18. # Construct from a value, another workaround
  19. if isinstance(v, int):
  20. return Item(value=v, multiplier=values["multiplier"])
  21. elif isinstance(v, Item):
  22. return Item(value=v.value, multiplier=values["multiplier"])
  23. if __name__ == '__main__':
  24. c = Container(
  25. multiplier=1,
  26. field_1=11,
  27. field_2=22
  28. )

答案2

得分: 1

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

  1. from pydantic import BaseModel, field_validator, model_validator
  2. from contextvars import ContextVar
  3. context_multiplier = ContextVar("context_multiplier")
  4. class Item(BaseModel):
  5. value: int
  6. @field_validator("value")
  7. @classmethod
  8. def validate_value(cls, value):
  9. multiplier = context_multiplier.get()
  10. if value % multiplier != 0:
  11. raise ValueError(f"not a multiple of {multiplier}")
  12. return value
  13. class Container(BaseModel):
  14. multiplier: int
  15. field_1: Item
  16. field_2: Item
  17. @model_validator(mode="wrap")
  18. @classmethod
  19. def validate_model(cls, v, handler):
  20. try:
  21. multiplier = int(v["multiplier"])
  22. except KeyError:
  23. raise ValueError("multiplier required")
  24. token = context_multiplier.set(v["multiplier"])
  25. try:
  26. return handler(v)
  27. finally:
  28. context_multiplier.reset(token)

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

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

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

  1. >>> Container.model_validate(
  2. ... {"multiplier": 3, "field_1": {"value": 9}, "field_2": {"value": 11}}
  3. ... )
  4. pydantic_core._pydantic_core.ValidationError: 1 validation error for Container
  5. field_2.value
  6. Value error, not a multiple of 3 [type=value_error, input_value=11, input_type=int]
  7. 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:

  1. from pydantic import BaseModel, field_validator, model_validator
  2. from contextvars import ContextVar
  3. context_multiplier = ContextVar("context_multiplier")
  4. class Item(BaseModel):
  5. value: int
  6. @field_validator("value")
  7. @classmethod
  8. def validate_value(cls, value):
  9. multiplier = context_multiplier.get()
  10. if value % multiplier != 0:
  11. raise ValueError(f"not a multiple of {multiplier}")
  12. return value
  13. class Container(BaseModel):
  14. multiplier: int
  15. field_1: Item
  16. field_2: Item
  17. @model_validator(mode="wrap")
  18. @classmethod
  19. def validate_model(cls, v, handler):
  20. try:
  21. multiplier = int(v["multiplier"])
  22. except KeyError:
  23. raise ValueError("multiplier required")
  24. token = context_multiplier.set(v["multiplier"])
  25. try:
  26. return handler(v)
  27. finally:
  28. context_multiplier.reset(token)

Validation succeeds with correct inputs:

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

Validation checks the inputs against the multiplier:

  1. >>> Container.model_validate(
  2. ... {"multiplier": 3, "field_1": {"value": 9}, "field_2": {"value": 11}}
  3. ... )
  4. pydantic_core._pydantic_core.ValidationError: 1 validation error for Container
  5. field_2.value
  6. Value error, not a multiple of 3 [type=value_error, input_value=11, input_type=int]
  7. 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:

确定