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

使SQLModel模型的TSTZRANGE字段兼容FastAPI响应序列化

解决FastAPI+SQLModel返回PostgreSQL TSTZRANGE类型序列化失败问题

问题原因

写入数据时,Pydantic能自动将Tuple[datetime, datetime]转换为PostgreSQL的TSTZRANGE类型存入数据库,但从数据库刷新对象后,ts_range字段会变成SQLAlchemy的Range对象,而Pydantic默认无法序列化这种对象,导致返回响应时出错。

解决方案

方案1:在模型中添加字段验证器(推荐)

通过Pydantic的validator装饰器,在序列化前自动将Range对象转换为Tuple类型,一劳永逸解决所有场景的序列化问题:

from datetime import datetime
from typing import Optional, Tuple

from sqlalchemy.dialects.postgresql import TSTZRANGE, Range
from sqlalchemy import Column
from sqlmodel import Field, SQLModel
from pydantic import validator


class DateRange(SQLModel):
    """Plan rate model"""

    id: Optional[int] = Field(default=None, primary_key=True)

    ts_range: Tuple[datetime, datetime] = Field(nullable=False, sa_column=Column(TSTZRANGE))

    @validator('ts_range', pre=False, always=True)
    def convert_range_to_tuple(cls, value):
        # 如果是SQLAlchemy的Range对象,转换为datetime元组
        if isinstance(value, Range):
            return (value.lower, value.upper)
        return value

方案2:接口内手动转换

在返回对象前,手动将Range对象转为Tuple,适合临时快速解决单个接口的问题:

@router.post(
    "/rates",
    tags=["rates", "create"],
    response_model=DateRange,
)
async def create_rate(
    plan_id: int,
    rate: DateRange,
    db: DB = Depends(get_db),
) -> DateRange:
    """Create new rate"""

    rate = DateRange(**rate.dict())
    db.add(rate)
    try:
        await db.commit()
        await db.refresh(rate)
        # 手动转换Range对象为datetime元组
        rate.ts_range = (rate.ts_range.lower, rate.ts_range.upper)
        return rate
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid date range [{e}]",
        )

方案3:自定义Pydantic字段类型

如果多个模型都需要使用TSTZRANGE类型,可以自定义一个专属字段类型,统一处理序列化逻辑:

from datetime import datetime
from typing import Tuple, Any
from sqlalchemy.dialects.postgresql import TSTZRANGE, Range
from sqlmodel import Field, SQLModel
from pydantic import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema


class TSTZRangeField(Tuple[datetime, datetime]):
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetJsonSchemaHandler) -> core_schema.CoreSchema:
        # 定义序列化逻辑:将Range对象转为datetime元组
        def serialize_range(value: Range) -> Tuple[datetime, datetime]:
            return (value.lower, value.upper)
        
        # 合并基础schema与自定义序列化逻辑
        base_schema = handler(source_type)
        return core_schema.json_serializer_function_override(
            core_schema.no_info_wrap_validator_function(
                lambda v: serialize_range(v) if isinstance(v, Range) else v,
                base_schema
            ),
            serialize_range
        )


# 在模型中使用自定义字段
class DateRange(SQLModel):
    """Plan rate model"""

    id: Optional[int] = Field(default=None, primary_key=True)

    ts_range: TSTZRangeField = Field(nullable=False, sa_column=Column(TSTZRANGE))

内容的提问来源于stack exchange,提问作者Pratik K.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 16:34:57