在使用FastAPI处理数据输入的时候,对模型数据和路径参数的一些转换处理
在开发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)
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)
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)
最后这种方式相对比较好,不过每次都要求进行一个函数的转换,着实不太方便,万一忘记了呢?所以我希望使用一个没有显著调用过程的实现,隐式的处理方式,也就是使用使用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
详细说明
-
模型定义:定义一个继承自
BaseModel
的 Pydantic 模型,如MyModel
。 -
使用
model_validator
:使用@model_validator
装饰器定义一个自定义的验证方法。在这个方法中,你可以将字段名转换为小写,以处理大小写不敏感的问题。mode='before'
:指定在模型创建之前执行此验证器。values
参数是一个字典,包含所有传入的数据字段。normalized_values
字典用于存储转换后的字段名和值。
-
创建模型实例:在 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]
主要原因是模型对象转换为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
通过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/"}
在上面的示例中,访问 /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/"}
在这个例子中,无论是 /items/
、/Items/
还是 /ITEMS/
,都将触发 read_items
函数,因为路径在中间件中被转换为小写。
这种实现方式会导致所有路由都不区分大小写,因此在设计路由时要考虑是否需要保持路径的区分。
3、在FastAPI的控制器处理中,提示获取不到request.user的值?
在 FastAPI 中,request.user
通常与身份验证系统相关,特别是在使用像 fastapi-users
或自定义认证中间件时。如果你在处理请求时无法获取 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)
其中DependsJwtAuth 就是要求通过Token验证的,否则提示权限不足,无法获得接口正常的数据。而它很简单的处理,如下代码
DependsJwtAuth = Depends(HTTPBearer())
由于我们在用户登录授权生成访问Token的时候,会返回相关的用户身份信息。
也就是验证的时候,可以获得用户的对象信息了
因此获得当前用户身份的信息代码,就可以正常工作了。
@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)
确保请求包含正确的身份验证信息(如 Authorization
header)。如果缺少或不正确,request.user
可能无法被填充。
如果我们确认用户身份,可以直接获得相关的用户属性信息了(模型中包含fullname属性等)。
username = request.user.fullname
这样我们可以通过中间件的方式,把用户身份信息提取出来,进行访问的日志的记录用途了。
我们在很多接口里面,都需要用户进行登录获取授权令牌,并设置请求头来确认令牌信息,才能进行下一步的操作接口,也就是FastAPI 中自定义用户身份验证逻辑,需要继承 AuthenticationBackend
类并实现 authenticate
方法。
首先,需要安装 starlette
,因为 AuthenticationBackend
是 Starlette
框架的一部分,而 FastAPI 本身是基于 Starlette
的。
最后通过处理验证后,可以返回相关的验证信息和用户对象。
return AuthCredentials(["authenticated"]), user
当然,我们也可以继承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
你可以创建一个自定义的 AuthenticationBackend
子类,并实现 authenticate
方法。这个方法接收一个 Request
对象,并返回一个包含 AuthCredentials
和 BaseUser
的元组。
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
最后,将自定义的认证后端添加到 FastAPI 应用中。使用 app.add_middleware
方法将认证后端集成到应用中。
app = FastAPI()
app.add_middleware(AuthenticationMiddleware, backend=CustomAuthBackend())
通过继承 AuthenticationBackend
,你可以在 FastAPI 中实现自定义的身份验证逻辑,并将其应用于整个应用程序。这样可以灵活地处理各种身份验证方案,如 JWT、OAuth、或自定义的认证方式。
转载请注明出处:撰写人:伍华聪 http://www.iqidi.com