FastAPI + Pydantic 防御式编程:90% 的人第一步就错了

在这里插入图片描述

看完这篇,你会用 Pydantic 把各种“奇葩请求”挡在门外,让接口更稳、报错更清楚。

开篇:来自前端的"惊喜"

你定义的接口是这样的:

@router.post("/generate")
async def generate(data: dict):
    prompt = data["prompt"]
    width = data["width"]
    height = data["height"]

你期望收到的请求是这样的:

{
    "prompt": "一个美丽的风景",
    "width": 1024,
    "height": 768
}

但生产环境实际收到的,让你怀疑人生:

{"prompt": null}
{"prompt": ""}
{"width": "1024"}
{"width": -100}
{"prompt": "test", "widht": 1024}
{}
{"prompt": "x"*100000}

翻译成人话:

前端传的后端的反应
nullAttributeError: 'NoneType'...
空字符串模型:???
字符串 “1024”TypeError 或者更诡异的行为
负数 -100模型直接罢工
字段拼错 widhtKeyError
空对象 {}KeyError: 'prompt'
10万字符内存爆炸

永远不要相信前端传来的数据。

这不是对前端同事的不信任,而是对墨菲定律的敬畏。


Pydantic:你的防弹衣

FastAPI 原生支持 Pydantic。把 dict 换成 Pydantic 模型,世界立刻清净了:

# app/models/schemas.py
from pydantic import BaseModel, Field
from typing import Optional

class GenerateShotImageRequest(BaseModel):
    """分镜图片生成请求"""

    # 必填字段:... 表示"你必须传"
    prompt: str = Field(
        ...,
        description="生成提示词",
        min_length=1,      # 不能是空字符串
        max_length=2000    # 别想撑爆我
    )

    # 可选字段:有默认值,不传也行
    width: int = Field(
        default=1024,
        description="图片宽度",
        ge=256,   # >= 256
        le=2048   # <= 2048
    )

    height: int = Field(
        default=1024,
        description="图片高度",
        ge=256,
        le=2048
    )

    seed: int = Field(
        default=-1,
        description="随机种子,-1 表示随机"
    )

    style: Optional[str] = Field(
        default=None,
        description="风格预设"
    )

使用起来无比丝滑:

@router.post("/shot-image")
async def generate_shot_image(request: GenerateShotImageRequest):
    # 走到这里,数据已经是:
    # - 类型正确 ✓
    # - 范围合法 ✓
    # - 必填字段存在 ✓

    result = await adapter.generate(
        prompt=request.prompt,   # 一定是非空字符串
        width=request.width,     # 一定是 256-2048 的整数
        height=request.height,
        seed=request.seed
    )
    return result

如果数据不合法,FastAPI 自动返回 422:

{
    "detail": [
        {
            "loc": ["body", "prompt"],
            "msg": "Field required",
            "type": "missing"
        },
        {
            "loc": ["body", "width"],
            "msg": "Input should be greater than or equal to 256",
            "type": "greater_than_equal"
        }
    ]
}

哪个字段有问题、什么问题,一清二楚。


自定义验证器:处理复杂逻辑

Field 约束不够用?上验证器:

from pydantic import BaseModel, field_validator, model_validator

class GenerateShotImageRequest(BaseModel):
    prompt: str
    width: int = 1024
    height: int = 1024

    @field_validator('prompt')
    @classmethod
    def prompt_must_not_be_empty(cls, v: str) -> str:
        """空白字符串?想得美"""
        if not v.strip():
            raise ValueError('Prompt cannot be empty or whitespace only')
        return v.strip()  # 顺手 trim 一下

    @field_validator('width', 'height')
    @classmethod
    def dimensions_must_be_multiple_of_64(cls, v: int) -> int:
        """某些模型要求尺寸是 64 的倍数"""
        if v % 64 != 0:
            v = (v // 64) * 64  # 自动修正,用户无感
        return v

    @model_validator(mode='after')
    def check_aspect_ratio(self) -> 'GenerateShotImageRequest':
        """宽高比别太离谱"""
        ratio = self.width / self.height
        if ratio > 4 or ratio < 0.25:
            raise ValueError('Aspect ratio must be between 1:4 and 4:1')
        return self

三种验证器

验证器作用适用场景
@field_validator验证单个字段格式检查、自动修正
@model_validator验证字段之间关系条件依赖、组合约束
返回值替换原值自动 trim、自动修正

枚举类型:只能传这几个值

from enum import Enum

class ShotType(str, Enum):
    WIDE = "wide"
    MEDIUM = "medium"
    CLOSE_UP = "close-up"
    EXTREME_CLOSE_UP = "extreme-close-up"

class ControlType(str, Enum):
    CANNY = "canny"
    DEPTH = "depth"
    POSE = "pose"

class GenerateShotImageRequest(BaseModel):
    prompt: str
    shot_type: ShotType = ShotType.MEDIUM
    control_type: Optional[ControlType] = None

前端传了个不存在的值:

{"shot_type": "super-close"}

直接打回:

{
    "detail": [{
        "loc": ["body", "shot_type"],
        "msg": "Input should be 'wide', 'medium', 'close-up' or 'extreme-close-up'",
        "type": "enum"
    }]
}

连可选值都帮你列出来了,前端改起来贼方便。


嵌套模型:套娃验证

复杂请求体?嵌套起来:

class AnimationParams(BaseModel):
    """动画参数"""
    duration: float = Field(default=3.0, ge=1.0, le=10.0)
    fps: int = Field(default=24, ge=12, le=60)
    camera_movement: Optional[str] = None

class CharacterRef(BaseModel):
    """角色参考"""
    image_path: str
    weight: float = Field(default=0.8, ge=0.0, le=1.0)

class GenerateVideoRequest(BaseModel):
    """视频生成请求"""
    shots: list[ShotData] = Field(..., min_length=1, max_length=100)
    animation: AnimationParams = Field(default_factory=AnimationParams)
    character_refs: list[CharacterRef] = Field(default_factory=list)

Pydantic 会递归验证每一层,一个都逃不掉。


条件验证:开关打开了,配置呢?

class GenerateShotImageRequest(BaseModel):
    prompt: str
    use_control_net: bool = False
    control_image_path: Optional[str] = None
    control_type: Optional[ControlType] = None

    @model_validator(mode='after')
    def validate_control_net(self) -> 'GenerateShotImageRequest':
        """开了 ControlNet 就必须传参考图"""
        if self.use_control_net:
            if not self.control_image_path:
                raise ValueError(
                    'control_image_path is required when use_control_net is True'
                )
            if not self.control_type:
                raise ValueError(
                    'control_type is required when use_control_net is True'
                )
        return self

再也不会出现"开关打开了但没传图"的尴尬。


响应模型:出口也要把关

不只是入口,出口也得验证:

class GenerationResult(BaseModel):
    """生成结果"""
    image_url: str
    seed: int
    generation_time: float

class GenerateShotImageResponse(BaseModel):
    """接口响应"""
    success: bool = True
    message: Optional[str] = None
    data: Optional[GenerationResult] = None


@router.post("/shot-image", response_model=GenerateShotImageResponse)
async def generate_shot_image(request: GenerateShotImageRequest):
    result = await generate(...)

    return GenerateShotImageResponse(
        success=True,
        data=GenerationResult(
            image_url=result["url"],
            seed=result["seed"],
            generation_time=result["time"]
        )
    )

响应模型的好处

  1. 自动过滤敏感字段——只返回定义的字段,内部字段不会泄露
  2. 自动生成 API 文档——Swagger UI 直接可用
  3. 格式一致性保证——前端永远知道会收到什么

美化验证错误响应

默认的 422 格式不太友好,改成和其他错误一致:

from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc: RequestValidationError):
    """把 422 变成好看的 400"""

    errors = []
    for error in exc.errors():
        field = ".".join(str(x) for x in error["loc"][1:])
        errors.append({
            "field": field,
            "message": error["msg"],
            "type": error["type"]
        })

    return JSONResponse(
        status_code=400,
        content={
            "success": False,
            "error": {
                "code": "VALIDATION_ERROR",
                "message": "Request validation failed",
                "details": errors
            }
        }
    )

现在返回:

{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Request validation failed",
        "details": [
            {
                "field": "prompt",
                "message": "Field required",
                "type": "missing"
            }
        ]
    }
}

和业务错误格式统一,前端处理起来舒服多了。


验证流程图

收到请求
JSON 格式正确?
400 JSON 解析错误
字段类型正确?
400 类型错误
Field 约束满足?
400 约束错误
自定义验证通过?
400 验证错误
进入业务逻辑

四层过滤,能进入业务逻辑的都是"好数据"。


验证规则速查表

收藏这张表,用的时候直接抄:

验证类型写法示例
必填Field(...)name: str = Field(...)
可选Optional[T]bio: Optional[str] = None
默认值Field(default=x)page: int = Field(default=1)
最小值ge= / gt=age: int = Field(ge=0)
最大值le= / lt=score: int = Field(le=100)
字符串长度min_length / max_lengthname: str = Field(min_length=1)
正则匹配pattern=email: str = Field(pattern=r".*@.*")
枚举值Enum 类型status: StatusEnum
列表长度min_length / max_lengthtags: list = Field(max_length=10)
单字段验证@field_validator见上文
跨字段验证@model_validator见上文

防御性编程六原则

  1. 所有接口都用 Pydantic 模型——dict 是万恶之源
  2. 必填字段用 ...——明确就是明确
  3. 数值加范围——防止负数、天文数字
  4. 字符串加长度——防止内存爆炸
  5. 复杂逻辑用验证器——保持模型定义清晰
  6. 响应也用模型——入口出口都把关

总结:信任是个奢侈品

在后端开发的世界里,"信任"是个奢侈品。

前端传的数据,可能是:

  • 用户手滑打错
  • 前端代码 bug
  • 恶意攻击
  • 网络传输出错

你的代码必须假设:所有输入都是恶意的,直到被证明是安全的。

Pydantic 就是那个帮你"验明正身"的安检员。

有了它:

  • KeyError 再见
  • TypeError 再见
  • AttributeError: 'NoneType'... 再见

你的业务代码只需要处理业务逻辑,不用再担心"这个值会不会是 null"。

这才是真正的"防御性编程"。


📌 我在公众号「程序员义拉冠」持续分享 AI 应用开发实战。点此 获取本系列合集

Matlab基于粒子群优化算法及鲁棒MPPT控制器提高光伏并网的效率内容概要:本文围绕Matlab在电力系统优化与控制领域的应用展开,重点介绍了基于粒子群优化算法(PSO)和鲁棒MPPT控制器提升光伏并网效率的技术方案。通过Matlab代码实现,结合智能优化算法与先进控制策略,对光伏发电系统的最大功率点跟踪进行优化,有效提高了系统在不同光照条件下的能量转换效率和并网稳定性。同时,文档还涵盖了多种电力系统应用场景,如微电网调度、储能配置、鲁棒控制等,展示了Matlab在科研复现与工程仿真中的强大能力。; 适合群:具备一定电力系统基础知识和Matlab编程能力的高校研究生、科研员及从事新能源系统开发的工程师;尤其适合关注光伏并网技术、智能优化算法应用与MPPT控制策略研究的专业士。; 使用场景及目标:①利用粒子群算法优化光伏系统MPPT控制器参数,提升动态响应速度与稳态精度;②研究鲁棒控制策略在光伏并网系统中的抗干扰能力;③复现已发表的高水平论文(如EI、SCI)中的仿真案例,支撑科研项目与学术写作。; 阅读建议:建议结合文中提供的Matlab代码与Simulink模型进行实践操作,重点关注算法实现细节与系统参数设置,同时参考链接中的完整资源下载以获取更多复现实例,加深对优化算法与控制系统设计的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员义拉冠

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值