展平嵌套 Pydantic 模型

nik*_*las 6 python pydantic fastapi

from typing import Union
from pydantic import BaseModel, Field


class Category(BaseModel):
    name: str = Field(alias="name")


class OrderItems(BaseModel):
    name: str = Field(alias="name")
    category: Category = Field(alias="category")
    unit: Union[str, None] = Field(alias="unit")
    quantity: int = Field(alias="quantity")
Run Code Online (Sandbox Code Playgroud)

当像这样实例化时:

OrderItems(**{'name': 'Test','category':{'name': 'Test Cat'}, 'unit': 'kg', 'quantity': 10})
Run Code Online (Sandbox Code Playgroud)

它返回这样的数据:

OrderItems(name='Test', category=Category(name='Test Cat'), unit='kg', quantity=10)
Run Code Online (Sandbox Code Playgroud)

但我想要这样的输出:

OrderItems(name='Test', category='Test Cat', unit='kg', quantity=10)
Run Code Online (Sandbox Code Playgroud)

我怎样才能实现这个目标?

Dan*_*erg 7

您应该尽可能尝试按照您实际希望数据最终呈现的方式来定义模式,而不是您从其他地方接收数据的方式。


更新:通用解决方案(一个或多个嵌套字段)

为了概括这个问题,我们假设您有以下模型:

from pydantic import BaseModel


class Foo(BaseModel):
    x: bool
    y: str
    z: int


class _BarBase(BaseModel):
    a: str
    b: float

    class Config:
        orm_mode = True


class BarNested(_BarBase):
    foo: Foo


class BarFlat(_BarBase):
    foo_x: bool
    foo_y: str
Run Code Online (Sandbox Code Playgroud)

问题:您希望能够使用BarFlatfoo一样的参数进行初始化BarNested,但数据最终会出现在平面模式中,其中字段foo_xfoo_y对应于模型上的x和(并且您对 不感兴趣)。yFooz

解决方案:定义一个自定义root_validatorpre=True检查foo数据中是否存在键/属性。如果是,它会根据Foo模型验证相应的对象,获取其x和值,然后使用它们通过和键y扩展给定的数据:foo_xfoo_y

from pydantic import BaseModel, root_validator
from pydantic.utils import GetterDict

...

class BarFlat(_BarBase):
    foo_x: bool
    foo_y: str

    @root_validator(pre=True)
    def flatten_foo(cls, values: GetterDict) -> GetterDict | dict[str, object]:
        foo = values.get("foo")
        if foo is None:
            return values
        # Assume `foo` must ba valid `Foo` data:
        foo = Foo.validate(foo)
        return {
            "foo_x": foo.x,
            "foo_y": foo.y,
        } | dict(values)
Run Code Online (Sandbox Code Playgroud)

请注意,我们需要在根验证器中更加小心pre=True,因为值始终以 a 的形式传递GetterDict,这是一个不可变的类似映射的对象。因此,我们不能像分配字典那样简单地为其分配新值foo_x/ 。foo_y但没有什么可以阻止我们以常规旧数据的形式返回清理后的数据dict.

为了进行演示,我们可以向其添加一些测试数据:

test_dict = {"a": "spam", "b": 3.14, "foo": {"x": True, "y": ".", "z": 0}}
test_orm = BarNested(a="eggs", b=-1, foo=Foo(x=False, y="..", z=1))
test_flat = '{"a": "beans", "b": 0, "foo_x": true, "foo_y": ""}'
bar1 = BarFlat.parse_obj(test_dict)
bar2 = BarFlat.from_orm(test_orm)
bar3 = BarFlat.parse_raw(test_flat)
print(bar1.json(indent=4))
print(bar2.json(indent=4))
print(bar3.json(indent=4))
Run Code Online (Sandbox Code Playgroud)

输出:

{
    "a": "spam",
    "b": 3.14,
    "foo_x": true,
    "foo_y": "."
}
Run Code Online (Sandbox Code Playgroud)
{
    "a": "eggs",
    "b": -1.0,
    "foo_x": false,
    "foo_y": ".."
}
Run Code Online (Sandbox Code Playgroud)
{
    "a": "beans",
    "b": 0.0,
    "foo_x": true,
    "foo_y": ""
}
Run Code Online (Sandbox Code Playgroud)

第一个例子模拟了一种常见的情况,数据以嵌套字典的形式传递给我们。第二个例子是典型的数据库ORM对象情况,其中BarNested代表我们在数据库中找到的模式。第三个只是为了表明我们仍然可以在BarFlat 没有参数的情况下正确初始化foo

需要注意的一个警告是,如果验证器foovalues. 如果您的模型配置了Extra.forbid该配置,将会导致错误。在这种情况下,您只需要额外的一行,将原始内容强制GetterDictdict第一个,然后pop是密钥"foo",而不是get对其进行设置。


原始帖子(展平单个字段)

如果您需要嵌套Category模型进行数据库插入,但您想要一个“平面”订单模型,并且category在响应中只是一个字符串,那么您应该将其拆分为两个单独的模型。

然后,在响应模型中,您可以定义一个自定义验证器来处理当您尝试初始化它并提供或forpre=True的实例时的情况。Categorydictcategory

这是我的建议:

from pydantic import BaseModel, validator


class Category(BaseModel):
    name: str


class OrderItemBase(BaseModel):
    name: str
    unit: str | None
    quantity: int


class OrderItemCreate(OrderItemBase):
    category: Category


class OrderItemResponse(OrderItemBase):
    category: str

    @validator("category", pre=True)
    def handle_category_model(cls, v: object) -> object:
        if isinstance(v, Category):
            return v.name
        if isinstance(v, dict) and "name" in v:
            return v["name"]
        return v
Run Code Online (Sandbox Code Playgroud)

这是一个演示:

if __name__ == "__main__":
    insert_data = '{"name": "foo", "category": {"name": "bar"}, "quantity": 1}'
    insert_obj = OrderItemCreate.parse_raw(insert_data)
    print(insert_obj.json(indent=2))
    ...  # insert into DB
    response_obj = OrderItemResponse.parse_obj(insert_obj.dict())
    print(response_obj.json(indent=2))
Run Code Online (Sandbox Code Playgroud)

这是输出:

{
  "name": "foo",
  "unit": null,
  "quantity": 1,
  "category": {
    "name": "bar"
  }
}
Run Code Online (Sandbox Code Playgroud)
{
  "name": "foo",
  "unit": null,
  "quantity": 1,
  "category": "bar"
}
Run Code Online (Sandbox Code Playgroud)

这种方法的好处之一是 JSON 架构与模型上的内容保持一致。如果您使用它,则FastAPI意味着 swagger 文档实际上将反映该端点的使用者收到的内容。您当然可以覆盖和自定义模式创建,但是......为什么呢?只要一开始就正确定义模型,就可以避免将来出现麻烦。

  • @MrNetherlands 是的,你是对的,这需要与常规的“dict”有点不同的处理方式。这没有记录([因为 Pydantic v2 即将推出](https://github.com/pydantic/pydantic/issues/4698) 无论如何)。我更新了我的答案,首先提出一个更通用的解决方案。 (2认同)