请求体 - 嵌套模型

使用 FastAPI,你可以定义、校验、记录文档并使用任意深度嵌套的模型(归功于Pydantic)。

List 字段

你可以将一个属性定义为拥有子元素的类型。例如 Python list

  1. from typing import Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Item(BaseModel):
  6. name: str
  7. description: Union[str, None] = None
  8. price: float
  9. tax: Union[float, None] = None
  10. tags: list = []
  11. @app.put("/items/{item_id}")
  12. async def update_item(item_id: int, item: Item):
  13. results = {"item_id": item_id, "item": item}
  14. return results

这将使 tags 成为一个由元素组成的列表。不过它没有声明每个元素的类型。

具有子类型的 List 字段

但是 Python 有一种特定的方法来声明具有子类型的列表:

从 typing 导入 List

首先,从 Python 的标准库 typing 模块中导入 List

  1. from typing import List, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Item(BaseModel):
  6. name: str
  7. description: Union[str, None] = None
  8. price: float
  9. tax: Union[float, None] = None
  10. tags: List[str] = []
  11. @app.put("/items/{item_id}")
  12. async def update_item(item_id: int, item: Item):
  13. results = {"item_id": item_id, "item": item}
  14. return results

声明具有子类型的 List

要声明具有子类型的类型,例如 listdicttuple

  • typing 模块导入它们
  • 使用方括号 [] 将子类型作为「类型参数」传入
  1. from typing import List
  2. my_list: List[str]

这完全是用于类型声明的标准 Python 语法。

对具有子类型的模型属性也使用相同的标准语法。

因此,在我们的示例中,我们可以将 tags 明确地指定为一个「字符串列表」:

  1. from typing import List, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Item(BaseModel):
  6. name: str
  7. description: Union[str, None] = None
  8. price: float
  9. tax: Union[float, None] = None
  10. tags: List[str] = []
  11. @app.put("/items/{item_id}")
  12. async def update_item(item_id: int, item: Item):
  13. results = {"item_id": item_id, "item": item}
  14. return results

Set 类型

但是随后我们考虑了一下,意识到标签不应该重复,它们很大可能会是唯一的字符串。

Python 具有一种特殊的数据类型来保存一组唯一的元素,即 set

然后我们可以导入 Set 并将 tag 声明为一个由 str 组成的 set

  1. from typing import Set, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Item(BaseModel):
  6. name: str
  7. description: Union[str, None] = None
  8. price: float
  9. tax: Union[float, None] = None
  10. tags: Set[str] = set()
  11. @app.put("/items/{item_id}")
  12. async def update_item(item_id: int, item: Item):
  13. results = {"item_id": item_id, "item": item}
  14. return results

这样,即使你收到带有重复数据的请求,这些数据也会被转换为一组唯一项。

而且,每当你输出该数据时,即使源数据有重复,它们也将作为一组唯一项输出。

并且还会被相应地标注 / 记录文档。

嵌套模型

Pydantic 模型的每个属性都具有类型。

但是这个类型本身可以是另一个 Pydantic 模型。

因此,你可以声明拥有特定属性名称、类型和校验的深度嵌套的 JSON 对象。

上述这些都可以任意的嵌套。

定义子模型

例如,我们可以定义一个 Image 模型:

  1. from typing import Set, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Image(BaseModel):
  6. url: str
  7. name: str
  8. class Item(BaseModel):
  9. name: str
  10. description: Union[str, None] = None
  11. price: float
  12. tax: Union[float, None] = None
  13. tags: Set[str] = set()
  14. image: Union[Image, None] = None
  15. @app.put("/items/{item_id}")
  16. async def update_item(item_id: int, item: Item):
  17. results = {"item_id": item_id, "item": item}
  18. return results

将子模型用作类型

然后我们可以将其用作一个属性的类型:

  1. from typing import Set, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Image(BaseModel):
  6. url: str
  7. name: str
  8. class Item(BaseModel):
  9. name: str
  10. description: Union[str, None] = None
  11. price: float
  12. tax: Union[float, None] = None
  13. tags: Set[str] = set()
  14. image: Union[Image, None] = None
  15. @app.put("/items/{item_id}")
  16. async def update_item(item_id: int, item: Item):
  17. results = {"item_id": item_id, "item": item}
  18. return results

这意味着 FastAPI 将期望类似于以下内容的请求体:

  1. {
  2. "name": "Foo",
  3. "description": "The pretender",
  4. "price": 42.0,
  5. "tax": 3.2,
  6. "tags": ["rock", "metal", "bar"],
  7. "image": {
  8. "url": "http://example.com/baz.jpg",
  9. "name": "The Foo live"
  10. }
  11. }

再一次,仅仅进行这样的声明,你将通过 FastAPI 获得:

  • 对被嵌入的模型也适用的编辑器支持(自动补全等)
  • 数据转换
  • 数据校验
  • 自动生成文档

特殊的类型和校验

除了普通的单一值类型(如 strintfloat 等)外,你还可以使用从 str 继承的更复杂的单一值类型。

要了解所有的可用选项,请查看关于 来自 Pydantic 的外部类型 的文档。你将在下一章节中看到一些示例。

例如,在 Image 模型中我们有一个 url 字段,我们可以把它声明为 Pydantic 的 HttpUrl,而不是 str

  1. from typing import Set, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel, HttpUrl
  4. app = FastAPI()
  5. class Image(BaseModel):
  6. url: HttpUrl
  7. name: str
  8. class Item(BaseModel):
  9. name: str
  10. description: Union[str, None] = None
  11. price: float
  12. tax: Union[float, None] = None
  13. tags: Set[str] = set()
  14. image: Union[Image, None] = None
  15. @app.put("/items/{item_id}")
  16. async def update_item(item_id: int, item: Item):
  17. results = {"item_id": item_id, "item": item}
  18. return results

该字符串将被检查是否为有效的 URL,并在 JSON Schema / OpenAPI 文档中进行记录。

带有一组子模型的属性

你还可以将 Pydantic 模型用作 listset 等的子类型:

  1. from typing import List, Set, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel, HttpUrl
  4. app = FastAPI()
  5. class Image(BaseModel):
  6. url: HttpUrl
  7. name: str
  8. class Item(BaseModel):
  9. name: str
  10. description: Union[str, None] = None
  11. price: float
  12. tax: Union[float, None] = None
  13. tags: Set[str] = set()
  14. images: Union[List[Image], None] = None
  15. @app.put("/items/{item_id}")
  16. async def update_item(item_id: int, item: Item):
  17. results = {"item_id": item_id, "item": item}
  18. return results

这将期望(转换,校验,记录文档等)下面这样的 JSON 请求体:

  1. {
  2. "name": "Foo",
  3. "description": "The pretender",
  4. "price": 42.0,
  5. "tax": 3.2,
  6. "tags": [
  7. "rock",
  8. "metal",
  9. "bar"
  10. ],
  11. "images": [
  12. {
  13. "url": "http://example.com/baz.jpg",
  14. "name": "The Foo live"
  15. },
  16. {
  17. "url": "http://example.com/dave.jpg",
  18. "name": "The Baz"
  19. }
  20. ]
  21. }

Info

请注意 images 键现在具有一组 image 对象是如何发生的。

深度嵌套模型

你可以定义任意深度的嵌套模型:

  1. from typing import List, Set, Union
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel, HttpUrl
  4. app = FastAPI()
  5. class Image(BaseModel):
  6. url: HttpUrl
  7. name: str
  8. class Item(BaseModel):
  9. name: str
  10. description: Union[str, None] = None
  11. price: float
  12. tax: Union[float, None] = None
  13. tags: Set[str] = set()
  14. images: Union[List[Image], None] = None
  15. class Offer(BaseModel):
  16. name: str
  17. description: Union[str, None] = None
  18. price: float
  19. items: List[Item]
  20. @app.post("/offers/")
  21. async def create_offer(offer: Offer):
  22. return offer

Info

请注意 Offer 拥有一组 Item 而反过来 Item 又是一个可选的 Image 列表是如何发生的。

纯列表请求体

如果你期望的 JSON 请求体的最外层是一个 JSON array(即 Python list),则可以在路径操作函数的参数中声明此类型,就像声明 Pydantic 模型一样:

  1. images: List[Image]

例如:

  1. from typing import List
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel, HttpUrl
  4. app = FastAPI()
  5. class Image(BaseModel):
  6. url: HttpUrl
  7. name: str
  8. @app.post("/images/multiple/")
  9. async def create_multiple_images(images: List[Image]):
  10. return images

无处不在的编辑器支持

你可以随处获得编辑器支持。

即使是列表中的元素:

请求体 - 嵌套模型 - 图1

如果你直接使用 dict 而不是 Pydantic 模型,那你将无法获得这种编辑器支持。

但是你根本不必担心这两者,传入的字典会自动被转换,你的输出也会自动被转换为 JSON。

任意 dict 构成的请求体

你也可以将请求体声明为使用某类型的键和其他类型值的 dict

无需事先知道有效的字段/属性(在使用 Pydantic 模型的场景)名称是什么。

如果你想接收一些尚且未知的键,这将很有用。


其他有用的场景是当你想要接收其他类型的键时,例如 int

这也是我们在接下来将看到的。

在下面的例子中,你将接受任意键为 int 类型并且值为 float 类型的 dict

  1. from typing import Dict
  2. from fastapi import FastAPI
  3. app = FastAPI()
  4. @app.post("/index-weights/")
  5. async def create_index_weights(weights: Dict[int, float]):
  6. return weights

Tip

请记住 JSON 仅支持将 str 作为键。

但是 Pydantic 具有自动转换数据的功能。

这意味着,即使你的 API 客户端只能将字符串作为键发送,只要这些字符串内容仅包含整数,Pydantic 就会对其进行转换并校验。

然后你接收的名为 weightsdict 实际上将具有 int 类型的键和 float 类型的值。

总结

使用 FastAPI 你可以拥有 Pydantic 模型提供的极高灵活性,同时保持代码的简单、简短和优雅。

而且还具有下列好处:

  • 编辑器支持(处处皆可自动补全!)
  • 数据转换(也被称为解析/序列化)
  • 数据校验
  • 模式文档
  • 自动生成的文档