跳到内容

请求体 - 嵌套模型

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

列表字段

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

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

这会使 tags 成为一个列表,尽管它没有声明列表中元素的类型。

带类型参数的列表字段

但 Python 有一种特定的方式来声明包含内部类型(即“类型参数”)的列表。

声明带类型参数的 list

若要声明带有类型参数(内部类型)的类型,如 listdicttuple,请使用方括号 [] 将内部类型作为“类型参数”传入:

my_list: list[str]

这是类型声明的标准 Python 语法。

在模型属性中使用相同的标准语法来声明内部类型。

因此,在我们的示例中,我们可以将 tags 明确定义为“字符串列表”:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

集合类型

仔细思考后,我们意识到标签(tags)不应该重复,它们应该是唯一的字符串。

Python 有一种专门用于存储唯一项的数据类型,即 set(集合)。

我们可以将 tags 声明为字符串集合:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

这样,即使你收到的请求包含重复数据,它也会被转换为唯一的集合项。

并且无论何时输出该数据,即使原始数据包含重复项,它也会作为唯一项集合输出。

它也会被相应地标注/记录。

嵌套模型

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

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

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

所有这些都可以任意嵌套。

定义子模型

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

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

将子模型用作类型

然后我们就可以把它用作属性的类型:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

这意味着 FastAPI 将期望收到类似如下的请求体:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

同样,仅通过这种声明,使用 FastAPI 你就能获得:

  • 编辑器支持(自动补全等),甚至是嵌套模型。
  • 数据转换
  • 数据验证
  • 自动文档

特殊类型与校验

除了像 strintfloat 等常规单一类型外,你还可以使用从 str 继承的更复杂的单一类型。

要查看所有可用选项,请查看 Pydantic 类型概览。你将在下一章看到一些示例。

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

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

该字符串将被校验是否为合法的 URL,并在 JSON Schema / OpenAPI 中进行相应记录。

包含子模型列表的属性

你也可以将 Pydantic 模型用作 listset 等的子类型。

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

这将期望(转换、校验、记录等)如下的 JSON 请求体:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": [
        "rock",
        "metal",
        "bar"
    ],
    "images": [
        {
            "url": "http://example.com/baz.jpg",
            "name": "The Foo live"
        },
        {
            "url": "http://example.com/dave.jpg",
            "name": "The Baz"
        }
    ]
}

信息

请注意 images 键现在包含一个图像对象列表。

深度嵌套模型

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

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


class Offer(BaseModel):
    name: str
    description: str | None = None
    price: float
    items: list[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

信息

请注意 Offer 如何包含一个 Item 列表,而 Item 又包含一个可选的 Image 列表。

纯列表请求体

如果你期望请求体的顶层是一个 JSON array(Python list),你可以在函数参数中声明该类型,方式与 Pydantic 模型相同:

images: list[Image]

正如:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images

全方位的编辑器支持

并且你处处都能获得编辑器支持。

即使是对列表内部的项目也是如此。

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

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

任意 dict 的请求体

你也可以将请求体声明为 dict,并指定键和值的类型。

这样,你无需预先知道有效的字段/属性名称(与 Pydantic 模型的情况不同)。

如果你想接收未知的键,这将非常有用。


另一种有用的情况是,你希望键是其他类型(例如 int)。

这就是我们要在这里看到的。

在这种情况下,你可以接收任何只要键为 int 且值为 floatdict

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
    return weights

提示

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

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

这意味着,即使你的 API 客户端只能发送字符串作为键,只要这些字符串包含纯整数,Pydantic 就会转换并校验它们。

并且你接收到的作为 weightsdict 实际上将拥有 int 类型的键和 float 类型的值。

总结

使用 FastAPI,你既能享有 Pydantic 模型提供的最大灵活性,又能保持代码简洁、简短且优雅。

同时还具备以下所有优点:

  • 编辑器支持(随处可见的自动补全!)
  • 数据转换(即解析/序列化)
  • 数据验证
  • Schema 文档
  • 自动文档