FastAPI处理特殊JSON数据:日期、循环引用与自定义对象的实战指南

FastAPI处理特殊JSON数据:日期、循环引用与自定义对象的实战指南

最近在重构一个内部数据管理平台时,我遇到了几个让人头疼的JSON序列化问题。API返回的数据里,datetime对象总是带着毫秒,前端同事抱怨格式不统一;更麻烦的是,用户关系数据中存在循环引用,直接序列化就会抛出RecursionError。还有那些自定义的ORM对象,Pydantic默认根本不认识它们。如果你也在FastAPI项目中遇到过类似的“非标准”JSON处理需求,这篇文章就是为你准备的。

我不会重复那些基础的“如何返回字典”的教程,而是聚焦于实际开发中真正棘手的场景:带时区的日期时间格式化、数据库模型间的循环依赖、以及如何让Pydantic优雅地处理你自定义的类。我们将深入jsonable_encoder的配置技巧,探索Pydantic模型的隐藏选项,并最终提供一套即插即用的解决方案模板。无论你是正在构建复杂的微服务,还是需要与特定前端规范对接,掌握这些技巧都能让你的API更加健壮和灵活。

1. 驯服日期与时间:超越默认的ISO格式

FastAPI和Pydantic默认将datetime对象序列化为ISO 8601格式的字符串,例如"2023-10-27T14:30:00.123456"。对于大多数国际化的应用,这很完美。但现实情况往往更复杂:你的前端可能要求"2023/10/27 14:30:00"这样的格式,或者你需要忽略毫秒部分,又或者你必须处理带时区的时间戳。直接返回包含datetime的字典会触发默认行为,而我们需要的是精细控制。

1.1 使用Pydantic模型的json_encoders配置

最优雅的方式是在Pydantic模型内部定义特定类型的序列化规则。每个Pydantic模型都有一个Config类,其中的json_encoders字段允许你为指定的Python类型注册自定义的序列化函数。

假设我们有一个活动(Event)模型,其中的start_timeend_time需要以"YYYY-MM-DD HH:MM:SS"的格式输出,并且忽略微秒。

from pydantic import BaseModel
from datetime import datetime
from typing import Optional

class EventResponse(BaseModel):
    id: int
    name: str
    start_time: datetime
    end_time: datetime
    description: Optional[str] = None

    class Config:
        json_encoders = {
            datetime: lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S")
        }

# 使用示例
event = EventResponse(
    id=1,
    name="技术分享会",
    start_time=datetime(2023, 10, 27, 14, 30, 5, 123456),
    end_time=datetime(2023, 10, 27, 16, 0, 0, 654321)
)

# 当FastAPI返回这个模型实例时,datetime字段会被自动转换
# 序列化结果中的时间字段将是 "2023-10-27 14:30:05" 和 "2023-10-27 16:00:00"

注意:json_encoders只在模型被序列化为JSON时生效(例如通过model_dump_json()或作为FastAPI响应返回时)。它不会影响模型实例在Python内部的属性值,event.start_time仍然是一个datetime对象。

1.2 处理时区感知的datetime对象

如果你的数据来自支持时区的数据库(如PostgreSQL的TIMESTAMPTZ),你得到的是timezone-awaredatetime对象。一个常见的需求是统一将其转换为UTC时间戳(毫秒数)或特定时区的字符串进行传输。

from pydantic import BaseModel
from datetime import datetime
import pytz  # 需要安装 pytz 包

class EventResponseWithTZ(BaseModel):
    id: int
    name: str
    local_start_time: datetime  # 假设是从数据库读出的带时区时间
    utc_timestamp: int = None   # 计算出的UTC时间戳

    class Config:
        json_encoders = {
            datetime: lambda dt: int(dt.timestamp() * 1000) if dt.tzinfo else dt.isoformat()
        }

    def __init__(self, **data):
        super().__init__(**data)
        # 在初始化时计算UTC时间戳
        if self.local_start_time and self.local_start_time.tzinfo:
            self.utc_timestamp = int(self.local_start_time.timestamp() * 1000)

# 假设从数据库读出的时间带有时区信息
db_time = datetime(2023, 10, 27, 14, 30, tzinfo=pytz.timezone('Asia/Shanghai'))
event_tz = EventResponseWithTZ(id=2, name="跨时区会议", local_start_time=db_time)

# 序列化后,local_start_time字段会变成如1666852200000的毫秒时间戳
# 而utc_timestamp字段是显式计算出的相同值

这里我们做了一个判断:如果datetime对象有时区信息,就转换为毫秒时间戳;否则,回退到默认的ISO格式。这提供了灵活性。

1.3 通过依赖注入实现全局日期格式控制

如果项目中有大量模型需要统一的日期格式,在每个模型里配置json_encoders会很繁琐。我们可以创建一个自定义的Pydantic基类,让所有响应模型继承它。

from pydantic import BaseModel as PydanticBaseModel
from datetime import datetime, date
from typing import Any

class BaseResponseModel(PydanticBaseModel):
    """所有API响应模型的基类,统一日期序列化格式"""
    
    class Config:
        json_encoders = {
            datetime: lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S"),
            date: lambda d: d.strftime("%Y-%m-%d")
        }
        # 允许使用ORM对象(如SQLAlchemy模型)初始化
        from_attributes = True

# 具体业务模型继承此基类即可
class UserResponse(BaseResponseModel):
    id: int
    username: str
    created_at: datetime
    birth_date: date

这种方法确保了整个API在日期格式上的一致性。from_attributes = True这个配置项也很有用,它允许你直接用SQLAlchemy等ORM的实例来初始化Pydantic模型,我们稍后会详细讨论。

2. 破解循环引用的死结

循环引用是图状数据结构(如社交网络、树形目录、双向关联)中常见的问题。例如,一个User对象有一个friends列表,列表中的每个Friend又是User对象。Python可以轻松表示这种结构,但JSON序列化器会陷入无限循环。

2.1 识别循环引用场景

让我们用一个简单的社交关系模型来演示这个问题:

from pydantic import BaseModel
from typing import List

class User(BaseModel):
    id: int
    name: str
    friends: List['User'] = []  # 这里产生了循环引用!

# 尝试创建循环引用
alice = User(id=1, name="Alice")
bob = User(id=2, name="Bob")
alice.friends.append(bob)
bob.friends.append(alice)  # 现在Alice和Bob互为好友

# 直接尝试序列化alice会失败
# json_str = alice.model_dump_json()  # 这将引发 RecursionError

当你运行上面的序列化代码时,Python的递归限制会被触发,因为序列化器会不断地尝试序列化alice,然后序列化她的朋友bob,然后又是bob的朋友alice,如此往复。

2.2 解决方案一:使用jsonable_encoderexclude参数

FastAPI提供的jsonable_encoder函数比Pydantic默认的序列化更智能,它内置了对循环引用的检测。当检测到循环引用时,它会默认跳过该字段。但更好的做法是显式地排除可能引起循环的字段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值