FastAPI 中为每个端点配置请求模型。

huangapple go评论85阅读模式
英文:

Configuring request model per endpoint in FastAPI

问题

我有一个 FastAPI 应用程序,在其中几个端点需要相同的输入模型,但在每个端点中,一些属性可以是可选的,而其他属性是必需的。例如:

# file: app/schemas/models.py
from pydantic import BaseModel

class GenericRequestModel(BaseModel):
    id: UUID = None      # 所有端点都需要的
    attr1: str = None    # 端点 1 和 2 需要的,端点 3 可选
    attr2: boot = None   # 端点 2 需要的,端点 1 和 3 可选
    attr3: int = None    # 所有端点可选

# file: app/api/endpoints.py
from fastapi import APIRouter

router = APIRouter(prefix='/api')

@router.post('/endpoint-1')
def endpoint_1(params: GenericRequestModel) -> ResponseModel:
    return calc_response_using_all_attrs(params)

@router.post('/endpoint-2')
def endpoint_2(params: GenericRequestModel) -> ResponseModel:
    return return calc_response_using_attrs_1_and_2(params)

@router.post('/endpoint-3')
def endpoint_3(params: GenericRequestModel) -> ResponseModel:
    return calc_generic_response(params)

是否可以根据端点配置 GenericRequestModel 的必需属性,而不需要为每个端点派生新的请求模型?如果不能,最优雅的解决方案是什么?

编辑
为了完整起见,这是我提出这个问题的理由。假设你有许多端点(例如 50 个)和许多属性(例如 100 个)。每个端点可能会对属性执行复杂的操作,某些端点可以克服缺少的数据。显然,我不想创建 50 个不同的模型。

英文:

I have a FastAPI application, in which several endpoints require the same input model, but in each, some attributes may be optional while others are required. For example:

# file: app/schemas/models.py
from pydantic import BaseModel

class GenericRequestModel(BaseModel):
    id: UUID = None      # required by all endpoints
    attr1: str = None    # required by endpoint 1 and 2, optional for 3
    attr2: boot = None   # required by 2, optional for 1, 3
    attr3: int = None    # optional by all


# file: app/api/endpoints.py
from fastapi import APIRouter

router = APIRouter(prefix='/api')


@router.post('/endpoint-1')
def endpoint_1(params: GenericRequestModel) -> ResponseModel:
    return calc_response_using_all_attrs(params)

@router.post('/endpoint-2')
def endpoint_2(params: GenericRequestModel) -> ResponseModel:
    return return calc_response_using_attrs_1_and_2(params)

@router.post('/endpoint-3')
def endpoint_3(params: GenericRequestModel) -> ResponseModel:
    return calc_generic_response(params)

Is is possible to configure GenericRequestModel's required property per endpoint, without deriving a new request models for each endpoint? If not, what is the most elegant solution?

EDIT
For completeness, here's the rationale behind my question. Assume you have many endpoints (say 50), and many attributes (100). Each endpoint may do complex stuff with the attributes and some endpoint can overcome missing data. Obviously, I don't want to create 50 different models.

答案1

得分: 3

以下是翻译好的部分:

最好的做法是使用继承。但是您将需要多个模型,例如:

from typing import Optional

from pydantic import BaseModel


class RequestModelBase(BaseModel):
    id: UUID
    attr1: Optional[int]
    attr2: Optional[boot]
    attr3: Optional[int]


class RequestModel1(RequestModelBase):
    attr1: int

class RequestModel2(RequestModel1):
    attr2: boot

然后您可以使用:

  • RequestModel1 用于端点 1
  • RequestModel2 用于端点 2
  • RequestModelBase 用于端点 3

(希望我正确理解了您对必需/可选属性的规则。)

英文:

The best thing that you can do here is: use inheritance. But you will need more than one model, e.g.

from typing import Optional

from pydantic import BaseModel


class RequestModelBase(BaseModel):
    id: UUID
    attr1: Optional[int]
    attr2: Optional[boot]
    attr3: Optional[int]


class RequestModel1(RequestModelBase):
    attr1: int

class RequestModel2(RequestModel1):
    attr2: boot

Then you can use:

  • RequestModel1 for endpoint 1
  • RequestModel2 for endpoint 2
  • RequestModelBase for endpoint 3

(I hope I understood your rules for required/optional attributes correctly.)

答案2

得分: 1

> "显然,我不想创建50个不同的模型。"

不,这并不显而易见。如果您的模式不同(是的,属性是否必需是一个重要的模式差异),那么您需要为该模式提供一个不同的模型。我不确定您还期望什么。

明智的继承显然可以将代码重复降到最低。但您无法避免在某处定义这种模式差异。

为了最大的清晰度,通过 @Rafael-WO 提出的显式继承和覆盖方法是理想的。这也允许您重新定义覆盖字段的潜在默认值。

我想从技术上讲,另一种选择是使用一个真正的通用模型,并在每个路由处理函数中动态指定这些不同字段的不同类型,就像这样:

from typing import Any, Generic, Optional, TypeVar

from pydantic.generics import GenericModel

T1 = TypeVar("T1", bound=Optional[str])
T2 = TypeVar("T2", bound=Optional[bool])

class GenericRequestModel(GenericModel, Generic[T1, T2]):
    id: int      # 所有端点都需要的属性
    attr1: T1    # 端点1和2需要的属性,端点3可选
    attr2: T2    # 端点2需要的属性,端点1和3可选
    attr3: Optional[int] = None    # 所有端点可选

from fastapi import FastAPI

app = FastAPI()

@app.post('/endpoint-1')
def endpoint_1(params: GenericRequestModel[str, Optional[bool]]) -> dict[str, Any]:
    return params.dict()

@app.post('/endpoint-2')
def endpoint_2(params: GenericRequestModel[str, bool]) -> dict[str, Any]:
    return params.dict()

@app.post('/endpoint-3')
def endpoint_3(params: GenericRequestModel[Optional[str], Optional[bool]]) -> dict[str, Any]:
    return params.dict()

但需要注意的是,这仅在这种情况下有效,因为诸如 Optional[str](或等效的 Union[str, None]str | None)的注释受到 Pydantic 机制的特殊处理,隐式地将 None 添加为默认值,使其成为可选字段(这意味着您不必指定 attr: Optional[str] = None= None 在技术上是多余的)。

如果您想指定除 None 之外的任何默认值,那么这种方法将不起作用,因为该默认值必须在实际模型创建时明确定义。所以,如果您希望 attr1 仅为 str 类型,并且该字段的默认值为 "abc",除了在模型命名空间中明确定义以外,没有其他方法来表示这一点。

这将我们带回到基于继承的方法。

英文:

> "Obviously, I don't want to create 50 different models."

No, that is not obvious. If your schema is different (and yes, whether an attribute is required or not is an important schematic difference), then you need to provide a different model for that schema. I am not sure what else you expect.

Smart inheritance obviously will help to reduce code duplication to a minimum. But you cannot get around defining that schematic difference somewhere.

For maximum clarity the explicit inherit-and-override approach by @Rafael-WO is ideal. This also allows you to redefine potential default values for the overridden fields.

I guess technically another alternative is to use an actual generic model and specify the different types for those varying fields dynamically in each route handler function like this:

from typing import Any, Generic, Optional, TypeVar

from pydantic.generics import GenericModel

T1 = TypeVar("T1", bound=Optional[str])
T2 = TypeVar("T2", bound=Optional[bool])


class GenericRequestModel(GenericModel, Generic[T1, T2]):
    id: int      # required by all endpoints
    attr1: T1    # required by endpoint 1 and 2, optional for 3
    attr2: T2    # required by 2, optional for 1, 3
    attr3: Optional[int] = None    # optional by all



from fastapi import FastAPI

app = FastAPI()


@app.post('/endpoint-1')
def endpoint_1(params: GenericRequestModel[str, Optional[bool]]) -> dict[str, Any]:
    return params.dict()


@app.post('/endpoint-2')
def endpoint_2(params: GenericRequestModel[str, bool]) -> dict[str, Any]:
    return params.dict()


@app.post('/endpoint-3')
def endpoint_3(params: GenericRequestModel[Optional[str], Optional[bool]]) -> dict[str, Any]:
    return params.dict()

But it is important to note that this only works in this case because an annotation like Optional[str] (or equivalent Union[str, None] or str | None) receives special treatment by the Pydantic machinery and implicitly adds None as the default value making it an optional field. (Meaning you don't have to specify attr: Optional[str] = None, the = None is technically redundant.)

So something like GenericRequestModel[Optional[str], Optional[bool]] indicates to the model that both fields attr1 and attr2 not only allow None as a value for those fields, but also that None is the default value, if no value is passed at all.

This will not work, if you want to specify any default value other than None because that default must be defined explicitly during creation of the actual model. So if you for example want attr1 to be only of type str and the field to have a default value of "abc", there is no way to express that other than by explicitly defining that in the model namespace.

Which brings us back to the inheritance-based approach.

huangapple
  • 本文由 发表于 2023年6月6日 15:35:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76412365.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定