在 pydantic 中更改输出别名是可能的吗?

33

设置:

# Pydantic Models

class TMDB_Category(BaseModel):
    name: str = Field(alias="strCategory")
    description: str = Field(alias="strCategoryDescription")


class TMDB_GetCategoriesResponse(BaseModel):
    categories: list[TMDB_Category]


@router.get(path="category", response_model=TMDB_GetCategoriesResponse)
async def get_all_categories():
    async with httpx.AsyncClient() as client:
        response = await client.get(Endpoint.GET_CATEGORIES)
        return TMDB_GetCategoriesResponse.parse_obj(response.json())

问题:
在创建响应时使用了别名,我想避免使用它。我只需要这个别名来正确映射传入的数据,但在返回响应时,我想使用实际字段名称。

实际响应:

{
  "categories": [
    {
      "strCategory": "Beef",
      "strCategoryDescription": "Beef is ..."
    },
    {
      "strCategory": "Chicken",
      "strCategoryDescription": "Chicken is ..."
    }
}

期望的响应:

{
  "categories": [
    {
      "name": "Beef",
      "description": "Beef is ..."
    },
    {
      "name": "Chicken",
      "description": "Chicken is ..."
    }
}

5
我实际上观察到的行为恰好相反,即默认情况下在.dict().json()中未使用别名。根据文档,它们是否被使用取决于by_alias布尔关键字参数。考虑到作者认为别名是“在您的应用程序中公开使用的字段名称与在JavaScript、API、解析文件等地方使用的名称之间的映射”,这个默认值确实很奇怪。 - bluenote10
2
我认为你想在你的路径装饰器中使用 response_model_by_alias=False,正如这个答案中提到的那样:https://dev59.com/q8Lra4cB1Zd3GeqPLIEG#69679104 - Garrett Motzner
未来的读者可能也会发现这个答案有帮助。 - undefined
6个回答

47
更新(2023-10-07):请查看问题的评论以获取其他答案,并在同一问题中查看this answer以获取pydantic 2.0或更新版本。
切换别名和字段名称,并使用allow_population_by_field_name model config选项:
class TMDB_Category(BaseModel):
    strCategory: str = Field(alias="name")
    strCategoryDescription: str = Field(alias="description")

    class Config:
        allow_population_by_field_name = True

让别名配置您想要返回的字段名称,但启用allow_population_by_field_name以便能够解析使用不同字段名称的数据。

2
考虑到您所展示的设置,我以后能否通过别名在代码中访问这些属性?例如,在解析后: r = TMDB_GetCategoriesResponse.parse_obj(response.json()) print(r.name) print(r.description) 或者我是否被迫使用这些可怕的 r.strCategoryr.strCategoryDescription - Rechu
@Rechu,据我所知,没有通过别名直接访问字段值的方法,您必须使用字段名称。所谓直接访问是指不将其导出到字典中,例如。请查看[问题#565](https://github.com/samuelcolvin/pydantic/issues/565),其中库作者解释了别名作为“在您的应用程序中公开使用的字段名称和名称之间的映射。 公开意味着在javascript中,在API中,在您解析的文件中等”。这与您的情况不符,因为您有两个不同的公共名称。 - Hernán Alarcón
这个解决方案是否与 Pylance 集成? - Ed1123

12

使用配置选项by_alias


from fastapi import FastAPI, Path, Query
from pydantic import BaseModel, Field

app = FastAPI()

class Item(BaseModel):
    name: str = Field(..., alias="keck")

@app.post("/item")
async def read_items(
    item: Item,
):
    return item.dict(by_alias=False)

给定请求:

{
  "keck": "string"
}

这会返回

{
  "name": "string"
}

这是有效的答案。 - rickythefox
2
如果在路由器的配置中设置了response_model,那么这将无法正常工作。就像OP所做的那样,@router.get(path="category", response_model=TMDB_GetCategoriesResponse),响应字段的名称将不同,并且会导致在返回响应时验证失败。 - Faizi

3
一种备选方案(可能不会像pydantic那样受欢迎)是使用除pydantic之外的反序列化库。例如,Dataclass Wizard库就支持这种特定用例。如果您需要与Field(alias=...)提供的相同往返行为,则可以将all参数传递给json_field函数。请注意,使用此类库,您会失去执行完整类型验证的能力,这可以说是pydantic最大的优点之一;但是,它确实以类似于pydantic的方式执行类型转换。我认为数据验证不太重要的原因如下。
  • 如果您正在自己构建和传递输入,则可以相信您知道自己在做什么,并且正在传递正确的数据类型。
  • 如果您从另一个API获取输入,则假设该API有良好的文档,您只需从其文档中获取示例响应,并使用该响应来模拟类结构。如果API清晰地记录其响应结构,则通常不需要进行任何验证。
  • 数据验证需要时间,因此与仅执行类型转换并捕获可能发生的任何错误而不事先验证输入类型相比,它可能会稍微减慢过程。

因此,为了演示上述用例,这里是一个简单的示例,使用dataclass-wizard库(该库依赖于dataclasses而不是pydantic模型):

from dataclasses import dataclass

from dataclass_wizard import JSONWizard, json_field


@dataclass
class TMDB_Category:
    name: str = json_field('strCategory')
    description: str = json_field('strCategoryDescription')


@dataclass
class TMDB_GetCategoriesResponse(JSONWizard):
    categories: list[TMDB_Category] 

运行该代码的方式如下:

input_dict = {
  "categories": [
    {
      "strCategory": "Beef",
      "strCategoryDescription": "Beef is ..."
    },
    {
      "strCategory": "Chicken",
      "strCategoryDescription": "Chicken is ..."
    }
  ]
}

c = TMDB_GetCategoriesResponse.from_dict(input_dict)
print(repr(c))
# TMDB_GetCategoriesResponse(categories=[TMDB_Category(name='Beef', description='Beef is ...'), TMDB_Category(name='Chicken', description='Chicken is ...')])

print(c.to_dict())
# {'categories': [{'name': 'Beef', 'description': 'Beef is ...'}, {'name': 'Chicken', 'description': 'Chicken is ...'}]}

性能测试

如果有人感兴趣,我已经设置了一个快速基准测试,以比较使用pydantic和仅使用dataclasses进行反序列化和序列化的时间:

from dataclasses import dataclass
from timeit import timeit

from pydantic import BaseModel, Field

from dataclass_wizard import JSONWizard, json_field


# Pydantic Models
class Pydantic_TMDB_Category(BaseModel):
    name: str = Field(alias="strCategory")
    description: str = Field(alias="strCategoryDescription")


class Pydantic_TMDB_GetCategoriesResponse(BaseModel):
    categories: list[Pydantic_TMDB_Category]


# Dataclasses
@dataclass
class TMDB_Category:
    name: str = json_field('strCategory', all=True)
    description: str = json_field('strCategoryDescription', all=True)


@dataclass
class TMDB_GetCategoriesResponse(JSONWizard):
    categories: list[TMDB_Category]


# Input dict which contains sufficient data for testing (100 categories)
input_dict = {
  "categories": [
    {
      "strCategory": f"Beef {i * 2}",
      "strCategoryDescription": "Beef is ..." * i
    }
    for i in range(100)
  ]
}

n = 10_000

print('=== LOAD (deserialize)')
print('dataclass-wizard: ',
      timeit('c = TMDB_GetCategoriesResponse.from_dict(input_dict)',
             globals=globals(), number=n))
print('pydantic:         ',
      timeit('c = Pydantic_TMDB_GetCategoriesResponse.parse_obj(input_dict)',
             globals=globals(), number=n))

c = TMDB_GetCategoriesResponse.from_dict(input_dict)
pydantic_c = Pydantic_TMDB_GetCategoriesResponse.parse_obj(input_dict)

print('=== DUMP (serialize)')
print('dataclass-wizard: ',
      timeit('c.to_dict()',
             globals=globals(), number=n))
print('pydantic:         ',
      timeit('pydantic_c.dict()',
             globals=globals(), number=n))

以下是基准测试结果(在Mac OS Big Sur,Python 3.9.0上进行测试):

=== LOAD (deserialize)
dataclass-wizard:  1.742989194
pydantic:          5.31538175
=== DUMP (serialize)
dataclass-wizard:  2.300118940
pydantic:          5.582638598

在他们的文档中,pydantic 声称是最快的库,但很容易证明不是这样。如上所述,对于上述数据集,pydantic 在反序列化和序列化过程中都慢了约2倍。值得注意的是,pydantic 已经非常快了。
< p > < em > 免责声明 :我是该库的创建者(和维护者)。< /p >

2
看起来你是dataclass-wizard的作者 - 最好添加一个免责声明,特别是因为你对竞争解决方案提出了关键性的要求。 - Seb
1
@Seb 很好的观点 - 真不敢相信我竟然忽略了那个。我刚刚在帖子中添加了免责声明,说明了这一点。 - rv.kvetch

3
你需要将alias更改为validation_alias
class TMDB_Category(BaseModel):
    name: str = Field(validation_alias="strCategory")
    description: str = Field(validation_alias="strCategoryDescription")

序列化别名可以使用serialization_alias进行设置。文档


0

我曾尝试过类似的事情(将字段 pattern 迁移到 patterns 列表,同时优雅地处理旧数据版本)。我能找到的最好解决方案是在 __init__ 方法中进行字段映射。按照OP的术语来说,就像这样:

class TMDB_Category(BaseModel):
    name: str
    description: str
    def __init__(self, **data):
        if "strCategory" in data:
            data["name"] = data.pop("strCategory")
        if "strCategoryDescription" in data:
            data["description"] = data.pop("strCategoryDescription")
        super().__init__(**data)

然后我们有:

>>> TMDB_Category(strCategory="name", strCategoryDescription="description").json()
'{"name": "name", "description": "description"}'

如果你需要使用字段别名来完成这个操作,但又想在代码中使用名称/描述字段,一种选择是修改Hernán Alarcón的解决方案,使用属性来实现:

class TMDB_Category(BaseModel):
    strCategory: str = Field(alias="name")
    strCategoryDescription: str = Field(alias="description")
    class Config:
        allow_population_by_field_name = True
    @property
    def name(self):
        return self.strCategory
    @name.setter
    def name(self, value):
        self.strCategory = value
    @property
    def description(self):
        return self.strCategoryDescription
    @description.setter
    def description(self, value):
        self.strCategoryDescription = value

这还是有点尴尬,因为repr使用了“别名”名称:

>>> TMDB_Category(name="name", description="description")
TMDB_Category(strCategory='name', strCategoryDescription='description')

0
也许你可以使用这种方法
from pydantic import BaseModel, Field


class TMDB_Category(BaseModel):
    name: str = Field(alias="strCategory")
    description: str = Field(alias="strCategoryDescription")


data = {
    "strCategory": "Beef",
    "strCategoryDescription": "Beef is ..."
}


obj = TMDB_Category.parse_obj(data)

# {'name': 'Beef', 'description': 'Beef is ...'}
print(obj.dict())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接