如何在 FastAPI 响应中包含非 Pydantic 类?

8
我希望在路由响应中包含一个自定义类。我的应用程序主要使用嵌套的pydantic.BaseModel,因此返回整个内容而不编写从内部数据表示到路由返回的转换将是不错的选择。
只要所有都继承自pydantic.BaseModel,这很简单,但是我在后端使用了一个无法做到这一点的Foo类,而且我也不能为此目的子类化它。 我是否可以以某种方式鸭子类型地定义该类,以使fastapi接受它? 目前我基本上有以下代码: main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Foo:
    """Foo holds data and can't inherit from `pydantic.BaseModel`."""
    def __init__(self, x: int):
        self.x = x


class Response(BaseModel):
    foo: Foo
    # plus some more stuff that doesn't matter right now because it works


@app.get("/", response_model=Response)
def root():
    return Response(foo=Foo(1))


if __name__ == '__main__':
    import uvicorn
    uvicorn.run("main:app")  # RuntimeError

你可以使用 BaseConfig.arbitrary_types_allowed = True - 参见 fastapi issue 2382 - michaPau
1
这样做会生成一个有效的pydantic类,但是当尝试获取响应或渲染OpenAPI页面时,仍然会出现运行时错误,因为fastapi不知道如何将Foo实例转换为json。由于需要添加验证器才能完成此操作,因此完成后您不再需要允许任意类型,因为它已经定义明确了。 - Arne
1
实际上,服务器甚至无法启动:fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class '__main__.Foo'> is a valid pydantic field type - Arne
1
在主应用程序文件中声明全局变量(BaseConfig.arbitrary_types_allowed = True),服务器可以正常启动并返回foo: x: 1(本地主机上的最小示例) - API定义也对我失败了。- 我只是评论了一下,因为我遇到了似乎谈论同样问题的问题... - michaPau
你说得对,我想知道为什么在全局设置任意类型与在“Response”类上设置它如此不同。我会简化我的回答。 - Arne
2个回答

7

虽然没有文档说明,但你可以让非pydantic类与fastapi一起使用。你需要做的是:

  1. 告诉pydantic可以使用任意类。它会尝试使用vars()将它们转换为JSON,因此只有直接的数据容器才能工作 - 不要使用property__slots__或者其他类似的东西[1]

  2. 创建一个代理BaseModel,并告诉Foo如果有人请求它的架构,就提供它 - 这就是fastapi的OpenAPI页面的做法。 我假设你也想让它们有效,因为它们很棒。

main.py

from fastapi import FastAPI
from pydantic import BaseModel, BaseConfig, create_model

app = FastAPI()
BaseConfig.arbitrary_types_allowed = True  # change #1


class Foo:
    """Foo holds data and can't inherit from `pydantic.BaseModel`."""    
    def __init__(self, x: int):
        self.x = x

    __pydantic_model__ = create_model("Foo", x=(int, ...))  # change #2


class Response(BaseModel):
    foo: Foo


@app.get("/", response_model=Response)
def root():
    return Response(foo=Foo(1))


if __name__ == '__main__':
    import uvicorn
    uvicorn.run("main:app")  # works

[1] 如果你想要更复杂的 JSON 化,就需要通过 Config.json_encoders 显式地提供给 Response 类。


你能提供一下 Config.json_encoders 的用法例子吗?比如说,如果类型是 psycopg2.extras.DateTimeTZRange - Zaffer
它甚至可以使用非基本类型。谢谢。 - cavalcantelucas
事实上,只有这一行就足够了:BaseConfig.arbitrary_types_allowed = True - cavalcantelucas
如果您看到了这个答案,您可能只需要启用ORM模式,并将您的Pydantic类映射到ORM类:https://docs.pydantic.dev/usage/models/#orm-mode-aka-arbitrary-class-instances - Samuel Prevost

1
这里是使用子类、验证器和额外模式的完整实现:
from psycopg2.extras import DateTimeTZRange as DateTimeTZRangeBase
from sqlalchemy.dialects.postgresql import TSTZRANGE
from sqlmodel import (
    Column,
    Field,
    Identity,
    SQLModel,
)

from pydantic.json import ENCODERS_BY_TYPE

ENCODERS_BY_TYPE |= {DateTimeTZRangeBase: str}


class DateTimeTZRange(DateTimeTZRangeBase):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        if isinstance(v, str):
            lower = v.split(", ")[0][1:].strip().strip()
            upper = v.split(", ")[1][:-1].strip().strip()
            bounds = v[:1] + v[-1:]
            return DateTimeTZRange(lower, upper, bounds)
        elif isinstance(v, DateTimeTZRangeBase):
            return v
        raise TypeError("Type must be string or DateTimeTZRange")

    @classmethod
    def __modify_schema__(cls, field_schema):
        field_schema.update(type="string", example="[2022,01,01, 2022,02,02)")


class EventBase(SQLModel):
    __tablename__ = "event"
    timestamp_range: DateTimeTZRange = Field(
        sa_column=Column(
            TSTZRANGE(),
            nullable=False,
        ),
    )


class Event(EventBase, table=True):
    id: int | None = Field(
        default=None,
        sa_column_args=(Identity(always=True),),
        primary_key=True,
        nullable=False,
    )


根据@Arne的解决方案,如果您使用的类型具有__slots__并且基本上没有办法获取dict,则需要添加自己的验证器和模式。

Github问题链接:https://github.com/tiangolo/sqlmodel/issues/235#issuecomment-1162063590


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