parse_obj在 Pydantic 中,字段是异构元组?



在Pydantic中,类

class Foo(BaseModel):
bar : str
baz : int

可以通过执行以下操作从元组["aaa", 3]导入:

[**{key: tr[i] for i, key in enumerate(fields.__TraceItem__.keys())})]

它也可以使用parse_obj{bar: "aaa", baz: 3}转换。但是,您如何导入将两者结合起来的东西呢?换句话说,给定类

class Bar(BaseModel):
f1: str
f2: float
f3: Boolean
class Qux(BaseModel):
field1: str
field2: float
field3: list[Bar]

如何将以下 JSON 转换为上面的Qux对象?

{
field1: "bar",
field2: 3.14,
field3: [
["aaa", 2.71, True],
["bbb", -1, False]
]
}

您可以使用validator.

正如评论中指出的那样,">在pydantic接受数据之前,总会有一些情况下可能需要进行转换",这对于与模型不完全匹配的JSON数据尤其如此。

[["aaa", 2.71, True], ["bbb", -1, False]]

是一个基元值列表,它绝对不是与

[
{'f1': 'aaa', 'f2': 2.71, 'f3': True}, 
{'f1': 'bbb', 'f2': -1.0, 'f3': False}
]

它更接近于实际Bar对象的列表。

对于此类情况,您可能需要自己实现解析。

选项 1

Bar有一个validator,它将列表解析为Bar的列表。

class Bar(BaseModel):
f1: str
f2: float
f3: bool
class Qux(BaseModel):
field1: str
field2: float
field3: list[Bar]
@validator("field3", pre=True)
def parse_field3_as_bar(cls, value):
# If value is already a list[Bar], then return as-is
if isinstance(value, list) and isinstance(value[0], Bar):
return value
# If not, try coercing into a list[Bar]
# Expect value to be ex. [["aaa", 2.71, True], ["bbb", -1, False]]
try:
bar_fields = Bar.__fields__.keys()
return [
Bar(**dict(zip(bar_fields, triplet))) 
for triplet in value
]
except Exception:
raise ValueError(f"Cannot convert to list[Bar]: {value!r}")
>>> from main import Bar, Qux
>>> d = {"field1": "bar", "field2": 3.14, "field3": [["aaa", 2.71, True], ['bbb', -1, False]]}
>>> q = Qux(**d)
>>> q
Qux(field1='bar', field2=3.14, field3=[Bar(f1='aaa', f2=2.71, f3=True), Bar(f1='bbb', f2=-1.0, f3=False)])

如果传递的value的数据格式错误:

>>> dx = {"field1": "bar", "field2": 3.14, "field3": [["not", "a"], ["list", "of", "Bar"], "values"]}
>>> q = Qux(**dx)
...
File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Qux
field3
Cannot convert to list[Bar]: [['not', 'a'], ['list', 'of', 'Bar'], 'values'] (type=value_error)

您在标题中提到了parse_obj,我假设您的意思是parse_obj辅助函数.
它与此配合得很好:

>>> d = {"field1": "bar", "field2": 3.14, "field3": [["aaa", 2.71, True], ['bbb', -1, False]]}
>>> Qux.parse_obj(d)
Qux(field1='bar', field2=3.14, field3=[Bar(f1='aaa', f2=2.71, f3=True), Bar(f1='bbb', f2=-1.0, f3=False)])

选项 2

Bar具有与选项 1相同的validator,但具有each_item=True,以便您可以一次(一次一个triplet)将一个列表项解析为Bar对象:

class Bar(BaseModel):
f1: str
f2: float
f3: bool
class Qux(BaseModel):
field1: str
field2: float
field3: list[Bar]
@validator("field3", pre=True, each_item=True)
def parse_field3_as_bar(cls, value):
# If value is already a Bar, then return as-is
if isinstance(value, Bar):
return value
# Try coercing value into a Bar
# Expect value to be ex. ['aaa', 2.71, True]
try:
bar_fields = Bar.__fields__.keys()
return Bar(**dict(zip(bar_fields, value)))
except Exception:
raise ValueError("Cannot convert to Bar:", value)
>>> from main import Bar, Qux
>>> d = {"field1": "bar", "field2": 3.14, "field3": [["aaa", 2.71, True], ['bbb', -1, False]]}
>>> q = Qux(**d)
>>> q
Qux(field1='bar', field2=3.14, field3=[Bar(f1='aaa', f2=2.71, f3=True), Bar(f1='bbb', f2=-1.0, f3=False)])

如果传递的value的数据格式错误:

>>> from main import Bar, Qux
>>> dx = {"field1": "bar", "field2": 3.14, "field3": [["aaa", 2.71, True], 9999]}
>>> q = Qux(**dx)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Qux
field3 -> 1
Cannot convert to Bar: 9999 (type=value_error)

您在标题中提到了parse_obj,我假设您指的是parse_obj辅助函数.
它与此同样有效:

>>> d = {"field1": "bar", "field2": 3.14, "field3": [["aaa", 2.71, True], ['bbb', -1, False]]}
>>> Qux.parse_obj(d)
Qux(field1='bar', field2=3.14, field3=[Bar(f1='aaa', f2=2.71, f3=True), Bar(f1='bbb', f2=-1.0, f3=False)])

作为旁注,我已经在评论中提到过,您在问题中提到了">元组">

可以从元组["aaa", 3]导入

这不是元组,我在您的任何示例代码和输入/输出数据中都没有看到任何元组。我不确定你是否知道,但在 Python 中,元组指的是特定的数据类型:https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences。

您拥有的基本上是一个列表列表,每个子列表只是其他数据类型的列表。(这就是为什么在我的回答中我将其称为triplet)。

我会根据 Gino 的答案采取更简单的方法——在Bar上添加一个类方法以允许从数组实例化,然后从Qux的验证器调用该方法:

from typing import List, Any
from pydantic import BaseModel, validator

class Bar(BaseModel):
f1: str
f2: float
f3: bool
@classmethod
def from_array(cls, values: List[Any]):
"""
Returns instance of class from an array of values.
NOTE: This method assumes all fields are present
ALSO: A hackier implementation of this method:
return cls(
f1=values[0],
f2=values[1],
f3=values[2]
)
"""
model_data = {
key: values[i]
for i, key in enumerate(cls.__fields__.keys())
}
return cls.parse_obj(model_data)

class Qux(BaseModel):
field1: str
field2: float
field3: List[Bar]
@validator('field3', each_item=True, pre=True)
def field3_from_array(cls, v: List[Any]):
"""
Validator for field3 which can be represented as
a list containing:
- `Bar` objects
- arrays (of Bar values)
- dicts (of Bar objects)
"""
if isinstance(v, Bar):
return v
elif isinstance(v, list):
return Bar.from_array(v)
elif isinstance(v, dict):
return Bar.parse_obj(v)

以下是它的实际效果:

>>> data = {
...     "field1": "bar",
...     "field2": 3.14,
...     "field3": [
...         ["aaa", 2.71, True],
...         ["bbb", -1, False],
...         {"f1": "ccc", "f2": 3.0, "f3": True}
...     ]
... }
>>> Qux.parse_obj(data)
Qux(field1='bar',field2=3.14, field3=[Bar(f1='aaa', f2=2.71, f3=True), Bar(f1='bbb', f2=-1.0, f3=False), Bar(f1='ccc', f2=3.0, f3=True)])