在开发Python的后端API平台的时候,为了兼容我SqlSugar开发的一些Winform端、BS端、UniApp端、WPF端等接入,由于部分是基于.net的处理,因此可能对于接入对象的属性为常见的Camel的驼峰命名规则,但是Python一般约定属性名称为小写,因此需要对这个模型进行兼容;另外默认FastAPI路由路径也是大小写敏感的,因此也需要做兼容处理,本篇随笔介绍使用FastAPI处理数据输入的时候,对模型数据和路径参数的一些转换处理。

1、默认Pydantic的大小处理

在 Pydantic 中,model_validate 方法用于验证和创建模型实例,并且默认情况下是大小写敏感的。也就是说,JSON 数据中的字段名需要与模型中的字段名完全匹配,包括大小写。

Pydantic 默认不支持直接取消字段名的大小写敏感性。为了处理字段名的大小写敏感问题,我们需要另外处理,有几种方式进行实现。

1)预处理 JSON 数据

在传递 JSON 数据到 model_validate 之前,手动将 JSON 数据中的字段名转换为模型所需的格式(例如,全部小写或全部大写)。

from pydantic import BaseModel, model_validate
from typing import Dict, Any

class MyModel(BaseModel):
    id: int
    name: str
    description: str

def preprocess_data(data: Dict[str, Any]) -> Dict[str, Any]:
    # 转换字段名为小写
    return {k.lower(): v for k, v in data.items()}

data = {
    "ID": 1,
    "NAME": "Test",
    "DESCRIPTION": "Sample description"
}

# 预处理数据
preprocessed_data = preprocess_data(data)

# 使用 model_validate 创建模型实例
model_instance = MyModel.model_validate(preprocessed_data)
print(model_instance)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
2)使用自定义字段别名

在 Pydantic 模型中使用字段别名来处理不同的字段名称。这种方法适用于字段名有明确且一致的变化情况(例如,使用不同的大小写风格)。

from pydantic import BaseModel, Field

class MyModel(BaseModel):
    id: int = Field(..., alias='ID')
    name: str = Field(..., alias='NAME')
    description: str = Field(..., alias='DESCRIPTION')

data = {
    "ID": 1,
    "NAME": "Test",
    "DESCRIPTION": "Sample description"
}

# 使用 model_validate 创建模型实例
model_instance = MyModel.model_validate(data)
print(model_instance)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
3)使用自定义数据解析

如果需要更复杂的字段名处理,你可以实现自定义解析逻辑。例如,通过编写一个函数来将数据字段名标准化为所需的格式。

from pydantic import BaseModel
from typing import Dict, Any

class MyModel(BaseModel):
    id: int
    name: str
    description: str

def normalize_keys(data: Dict[str, Any]) -> Dict[str, Any]:
    # 自定义字段名标准化规则
    normalized_data = {}
    for key, value in data.items():
        normalized_key = key.lower()  # 或其他规则
        normalized_data[normalized_key] = value
    return normalized_data

data = {
    "ID": 1,
    "NAME": "Test",
    "DESCRIPTION": "Sample description"
}

# 标准化数据
normalized_data = normalize_keys(data)

# 使用 model_validate 创建模型实例
model_instance = MyModel.model_validate(normalized_data)
print(model_instance)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

最后这种方式相对比较好,不过每次都要求进行一个函数的转换,着实不太方便,万一忘记了呢?所以我希望使用一个没有显著调用过程的实现,隐式的处理方式,也就是使用使用model_validator进行隐式的转换处理。

model_validator 是 Pydantic v2 中用于模型验证的功能。要使用 model_validator 来处理字段名大小写不敏感的问题,你需要在模型中实现自定义的验证逻辑,以将字段名标准化为一致的格式(如小写)。

以下是如何使用 model_validator 处理字段名大小写不敏感的示例:

from pydantic import BaseModel, model_validator
from typing import Dict, Any

class MyModel(BaseModel):
    id: int
    name: str
    description: str

    @model_validator(mode='before')
    def normalize_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        # 将字段名转换为小写
        normalized_values = {}
        for key, value in values.items():
            normalized_key = key.lower()
            normalized_values[normalized_key] = value
        return normalized_values

# FastAPI 路由
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/items/")
async def create_item(request: Request):
    data = await request.json()
    model_instance = MyModel.model_validate(data)  # 使用 model_validate 创建模型实例
    return model_instance
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

详细说明

  1. 模型定义:定义一个继承自 BaseModel 的 Pydantic 模型,如 MyModel
  2. 使用 model_validator:使用 @model_validator 装饰器定义一个自定义的验证方法。在这个方法中,你可以将字段名转换为小写,以处理大小写不敏感的问题。
  • mode='before':指定在模型创建之前执行此验证器。
  • values 参数是一个字典,包含所有传入的数据字段。
  • normalized_values 字典用于存储转换后的字段名和值。
  1. 创建模型实例:在 FastAPI 路由处理函数中,使用 MyModel.model_validate(data) 创建模型实例。这里 data 是原始的 JSON 数据,经过 model_validator 处理后,字段名会被标准化为小写。

但是这样对于获得数据库对象,并转换为DTO对象(或者Schema对象)的时候,会导致模型转换出现问题,如下FastAPI的处理出现问题。

totalCount, items = await self.crud.get_list(input, db)
  pydantic_items = [self.dto_class.model_validate(item) for item in items]
  • 1.
  • 2.

主要原因是模型对象转换为dict类型的时候出现错误,因此需要限定转换的对象为dict类型,修改下基类的模型处理如下所示。

class SchemaBase(BaseModel):
    """定义的DOT类基类,统一处理一些操作,如大小写不敏感,枚举值处理等"""

    model_config = ConfigDict(use_enum_values=True, from_attributes=True)

    # 要实现字段名的大小写不敏感,你可以在模型中使用 model_validator 来处理字段名的标准化。
    @model_validator(mode="before")
    def normalize_keys(cls, values: Any) -> Dict[str, Any]:
        # 检查请求体是否为空
        if not values:
            raise ValueError("Empty request body")

        # 如果 values 是 dict 类型,将其键名转换为小写
        if isinstance(values, dict):
            return {key.lower(): value for key, value in values.items()}

        else:
            return values
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

通过Python的继承关系处理,我们所有子类对象,都可以实现查询参数的无感的小写转换,而不影响数据库对象的转换。

转换注意

在 Pydantic v2 中,ConfigDict 是用于配置 Pydantic 模型行为的一个机制。str_to_lower 配置项用于将输入字符串转换为小写,但它主要适用于字符串类型字段的值,而不是字段名。

如果你需要实现模型字段名的大小写不敏感,你可以使用 model_validator 进行自定义处理。

另外,如果仅仅单独使用对request.query_params的键转换小写,那么在Post请求获得的Body内容,无法进行大小写转换的,而且可能触发Body内容提前被消耗而导致再次读取的时候错误,但是使用model_validator 进行自定义处理则是可以的,因此model_validator 是比较推荐的处理方式。

 

2、对路由路径大小写转换处理

 在 FastAPI 中,定义路由路径时,路径是大小写敏感的。这意味着 /items//Items/ 被视为两个不同的路径。如果你希望路由路径不区分大小写,需要在代码中进行自定义处理,因为 FastAPI 不原生支持这一特性。

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
async def read_items():
    return {"message": "This is /items/"}

@app.get("/Items/")
async def read_items_uppercase():
    return {"message": "This is /Items/"}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

在上面的示例中,访问 /items//Items/ 会触发不同的路由处理函数。

如果你希望所有路由路径都不区分大小写,可以使用中间件来实现。例如,可以编写一个中间件,将请求路径转换为小写。

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()

class LowercaseMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 将路径转换为小写
        request.scope["path"] = request.scope["path"].lower()
        response = await call_next(request)
        return response

app.add_middleware(LowercaseMiddleware)

@app.get("/items/")
async def read_items():
    return {"message": "This is /items/"}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

在这个例子中,无论是 /items//Items/ 还是 /ITEMS/,都将触发 read_items 函数,因为路径在中间件中被转换为小写。

这种实现方式会导致所有路由都不区分大小写,因此在设计路由时要考虑是否需要保持路径的区分。

 

3、在FastAPI的控制器处理中,提示获取不到request.user的值?

在 FastAPI 中,request.user 通常与身份验证系统相关,特别是在使用像 fastapi-users 或自定义认证中间件时。如果你在处理请求时无法获取 request.user 的值,可能有以下几个原因:

1)确保身份验证依赖项正确配置

request.user 通常依赖于身份验证依赖项或中间件。例如,如果你使用 OAuth2 或 JWT 验证,需要确保正确设置了依赖项以填充 request.user

 例如我们在FastAPI的路由器中定义一个接口,我们要求该接口读取用户的身份信息(通过token获取身份信息)

# 根据名称获取客户
@router.get(
    "/by-name",
    response_model=AjaxResponse[CustomerDto | None],
    summary="根据名称获取客户",
    dependencies=[DependsJwtAuth],
)
async def get_by_name(
    name: Annotated[str | None, Query()] = None,
    db: AsyncSession = Depends(get_db),
):
    item = await customer_crud.get_by_name(name, db=db)
    item = jsonable_encoder(item)
    return AjaxResponse(success=True, result=item)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

其中DependsJwtAuth 就是要求通过Token验证的,否则提示权限不足,无法获得接口正常的数据。而它很简单的处理,如下代码

DependsJwtAuth = Depends(HTTPBearer())
  • 1.

由于我们在用户登录授权生成访问Token的时候,会返回相关的用户身份信息。

也就是验证的时候,可以获得用户的对象信息了

在使用FastAPI处理数据输入的时候,对模型数据和路径参数的一些转换处理_数据

因此获得当前用户身份的信息代码,就可以正常工作了。

@router.get(
    "/me",
    summary="获取当前用户信息",
    response_model=AjaxResponse,
    dependencies=[DependsJwtAuth],
    response_model_exclude={"password"},
)
async def get_current_user(request: Request):
    data = GetCurrentUserInfoDetail(**request.user.model_dump())
    return AjaxResponse(data)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

确保请求包含正确的身份验证信息(如 Authorization header)。如果缺少或不正确,request.user 可能无法被填充。

如果我们确认用户身份,可以直接获得相关的用户属性信息了(模型中包含fullname属性等)。

username = request.user.fullname
  • 1.

这样我们可以通过中间件的方式,把用户身份信息提取出来,进行访问的日志的记录用途了。

我们在很多接口里面,都需要用户进行登录获取授权令牌,并设置请求头来确认令牌信息,才能进行下一步的操作接口,也就是FastAPI 中自定义用户身份验证逻辑,需要继承 AuthenticationBackend 类并实现 authenticate 方法。

首先,需要安装 starlette,因为 AuthenticationBackendStarlette 框架的一部分,而 FastAPI 本身是基于 Starlette 的。

在使用FastAPI处理数据输入的时候,对模型数据和路径参数的一些转换处理_自定义_02

 最后通过处理验证后,可以返回相关的验证信息和用户对象。

 

return AuthCredentials(["authenticated"]), user
  • 1.

当然,我们也可以继承BaseUser来获得一些基础信息,返回这个用户对象信息。

class SimpleUser(BaseUser):
    def __init__(self, username: str):
        self.username = username

    @property
    def is_authenticated(self) -> bool:
        return True  # 用户是经过身份验证的

    @property
    def display_name(self) -> str:
        return self.username
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

你可以创建一个自定义的 AuthenticationBackend 子类,并实现 authenticate 方法。这个方法接收一个 Request 对象,并返回一个包含 AuthCredentialsBaseUser 的元组。

class CustomAuthBackend(AuthenticationBackend):
    async def authenticate(self, request: Request):
        # 从请求头获取认证信息
        auth_header: Optional[str] = request.headers.get("Authorization")

        if auth_header is None or not auth_header.startswith("Bearer "):
            return None  # 如果没有认证信息,返回 None 表示没有通过认证

        token = auth_header[len("Bearer "):]  # 提取令牌

        # 在这里添加你的令牌验证逻辑,例如验证 JWT 或从数据库中查询用户
        if token == "valid_token":  # 示例条件,应该替换为实际验证逻辑
            return AuthCredentials(["authenticated"]), SimpleUser("username")

        return None  # 认证失败时返回 None
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

最后,将自定义的认证后端添加到 FastAPI 应用中。使用 app.add_middleware 方法将认证后端集成到应用中。

app = FastAPI()
app.add_middleware(AuthenticationMiddleware, backend=CustomAuthBackend())
  • 1.
  • 2.

通过继承 AuthenticationBackend,你可以在 FastAPI 中实现自定义的身份验证逻辑,并将其应用于整个应用程序。这样可以灵活地处理各种身份验证方案,如 JWT、OAuth、或自定义的认证方式。