You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI上传文件返回422 Unprocessable Entity错误如何解决?

问题原因

  • HTTP协议限制:当接口包含UploadFile类型的文件参数时,请求的Content-Type必须为multipart/form-data。你当前代码中定义的request: schemas.CategoryIn/request: schemas.ProductIn属于JSON请求体参数,FastAPI会默认从application/json类型的请求体中解析这类参数,两种Content-Type无法共存,因此FastAPI找不到对应的request参数,返回422校验错误。
  • 入参Schema定义不合理:你当前的CategoryIn和ProductIn中image字段为必填项,但前端调用上传接口时只会传业务字段和文件,不会提前传入image的URL(这个URL是后端上传到云存储后才生成的),因此Pydantic对request参数做校验时会因为缺少必填的image字段报错,也会触发422错误。

解决方案

第一步:调整入参Schema,移除不需要前端传入的image字段

修改你的Schema定义,将入参类和基础类拆分,入参类不要包含image字段:

from typing import Optional, List
from pydantic import BaseModel

# 保留原有基础类,用于出参/ORM映射
class CategoryBase(BaseModel):
    name: str
    slug: str
    image: str

class Category(CategoryBase):
    id: int
    class Config:
        orm_mode = True

# 修改入参类,仅保留需要前端传入的字段
class CategoryIn(BaseModel):
    name: str
    slug: str

# Product部分同理修改
class ProductBase(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    image: str
    slug: str

class Product(ProductBase):
    id: int
    category_id: int
    category: Category
    class Config:
        orm_mode = True

# 修改Product入参类,移除image字段
class ProductIn(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    slug: str

# 出参类保持不变即可
class CategoryOut(CategoryBase):
    products: List[Product] = []
    id: int
    class Config:
        orm_mode = True

第二步:调整路由参数,兼容文件上传的请求格式

这里提供两种可选实现:

方案1:直接拆分参数为Form类型(简单直观,适合字段少的场景)

修改路由函数的参数定义,将原来的request对象拆分为单独的Form参数,手动构造入参实例:

from fastapi import Form

@router.post('/category/create', response_model=schemas.CategoryOut)
def create_category(
    *, 
    name: str = Form(...),
    slug: str = Form(...),
    file: UploadFile = File(...),  
    db: Session = Depends(get_db), 
    current_user: schemas.UserOut = Depends(get_current_active_user)
):
    category = storeview.get_category_by_slug(db, slug)
    if category:
        raise HTTPException(status.HTTP_400_BAD_REQUEST,
                            detail=f"Category with slug {slug} already exists!")
    # 手动构造CategoryIn实例,传入后端生成的image URL
    request = schemas.CategoryIn(
        name=name,
        slug=slug,
        image=storeview.save_file(file)
    )
    return storeview.create_category(db, request)


@router.post('/product/create', response_model=schemas.Product)
async def create_product(
    *, 
    file: UploadFile = File(...), 
    name: str = Form(...),
    description: Optional[str] = Form(None),
    price: float = Form(...),
    slug: str = Form(...),
    category_id: int = Form(...),
    db: Session = Depends(get_db), 
    current_user: schemas.UserOut = Depends(get_current_active_user)
):
    product = storeview.get_category_by_slug(db, slug)
    if product:
        raise HTTPException(status.HTTP_400_BAD_REQUEST,
                            detail=f"Product with slug {slug} already exists!")
    # 手动构造ProductIn实例
    request = schemas.ProductIn(
        name=name,
        description=description,
        price=price,
        slug=slug,
        image=storeview.save_file(file)
    )
    return storeview.create_product(db, request, category_id)

方案2:用Depends封装Pydantic解析(适合字段多不想拆分参数的场景)

你可以自定义一个依赖函数,从Form参数中解析出Pydantic模型实例,避免拆分大量参数:

import inspect
from fastapi import Form, Depends
from pydantic import BaseModel

def form_body(cls: BaseModel):
    async def as_form(**data):
        return cls(**data)
    sig = inspect.signature(as_form)
    parameters = []
    for k, field in cls.__fields__.items():
        if field.required:
            param = inspect.Parameter(k, inspect.Parameter.POSITIONAL_OR_KEYWORD, default=Form(...))
        else:
            param = inspect.Parameter(k, inspect.Parameter.POSITIONAL_OR_KEYWORD, default=Form(field.default))
        parameters.append(param)
    as_form.__signature__ = sig.replace(parameters=parameters)
    return as_form

# 给入参类加上form_body装饰器
@form_body
class CategoryIn(BaseModel):
    name: str
    slug: str

@form_body
class ProductIn(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    slug: str

之后路由就可以直接用Depends获取入参实例了:

@router.post('/category/create', response_model=schemas.CategoryOut)
def create_category(
    *, 
    request: schemas.CategoryIn = Depends(),  
    file: UploadFile = File(...),  
    db: Session = Depends(get_db), 
    current_user: schemas.UserOut = Depends(get_current_active_user)
):
    category = storeview.get_category_by_slug(db, request.slug)
    if category:
        raise HTTPException(status.HTTP_400_BAD_REQUEST,
                            detail=f"Category with slug {request.slug} already exists!")
    request.image = storeview.save_file(file)
    return storeview.create_category(db, request)

内容的提问来源于stack exchange,提问作者Seyi Oyefeso

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.08 11:09:04