SQLAlchemy查询结果需转为字典等基础类型才能JSON序列化,因ORM对象含_sa_instance_state等非序列化属性;推荐用model_to_dict()过滤清洗,或row._mapping适配不同查询结果,避免JSONEncoder.default全局误用。

因为 SQLAlchemy 的查询结果(无论是 User 实例还是 Query 返回的元组)不是原生 Python 基础类型,而 json.dumps() 和 flask.jsonify() 只认 dict、list、str、int、float、bool、None 这几种。ORM 对象自带状态管理、延迟加载属性、内部元数据(如 _sa_instance_state),这些全都不在 JSON 序列化白名单里。
to_dict() 方法最直接,但字段要手动列全
给模型加一个 to_dict() 是最快能跑通的方式,但它隐含几个实际坑:
- 每次新增字段都得同步改
to_dict(),容易漏 —— 尤其是后期加了DateTime、Decimal或外键关联字段时 -
datetime类型会直接报TypeError: Object of type datetime is not JSON serializable,得手动转成字符串 - 如果模型用了
db.relationship(),默认不会包含关联对象,但如果你误写了self.posts进去,又没处理懒加载,可能触发额外查询甚至循环引用 - 字段名硬编码,重构列名时容易不一致
示例写法(带基础类型防护):
def to_dict(self):
return {
"id": self.id,
"app_key": self.app_key,
"secret": self.secret,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
用 __dict__ + 过滤比手写 to_dict() 更省事,但有陷阱
很多人第一反应是用 obj.__dict__,它确实自动包含所有实例属性,但必须过滤掉 SQLAlchemy 的内部字段,否则会出错:
-
_sa_instance_state是必删项,不删就报错 -
query、query_class、metadata这些类属性也会混进来,需排除 - 延迟加载字段(如未访问过的
user.profile)在__dict__里可能是sqlalchemy.orm.attributes.InstrumentedAttribute,不能直接序列化 -
None值本身可序列化,但某些前端框架对null处理不友好,常需要转空字符串或默认值
安全过滤写法(推荐封装成工具函数):
def model_to_dict(obj):
d = {k: v for k, v in obj.__dict__.items() if not k.startswith('_') and k != 'metadata'}
# 清洗常见非序列化类型
for k, v in d.items():
if hasattr(v, 'isoformat'): # datetime/date
d[k] = v.isoformat()
elif isinstance(v, Decimal):
d[k] = float(v)
elif v is None:
d[k] = ""
return d
query.all() 返回的是对象列表,但 db.session.query(User.id, User.name) 返回的是元组
这是最容易被忽略的兼容性断点:两种查询返回结构完全不同,不能共用同一套序列化逻辑。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
-
User.query.all()→[<user>, <user>]</user></user>,每个元素是 ORM 实例,可用to_dict()或model_to_dict() -
db.session.query(User.id, User.name).all()→[(1, 'Alice'), (2, 'Bob')],每个元素是sqlalchemy.util._collections.result或普通元组,得用row._mapping(新版本)或row._asdict()(旧版本),或者手动解包 - 混合查询(如
db.session.query(User, Post.title))返回的是元组,第一个元素是User实例,第二个是字符串,必须分别处理
统一处理建议(适配新旧 SQLAlchemy):
def query_result_to_list(result):
if not result:
return []
first = result[0]
if hasattr(first, '_mapping'): # SQLAlchemy 1.4+
return [dict(row._mapping) for row in result]
elif hasattr(first, '_asdict'): # SQLAlchemy 1.3-
return [row._asdict() for row in result]
else: # ORM 实例列表
return [model_to_dict(row) for row in result]
别碰 json.JSONEncoder.default —— 它救不了所有场景
有人写自定义 JSONEncoder 拦截所有对象,看似一劳永逸,但实际问题更多:
- 它无法区分“这个
datetime是模型字段”还是“这个datetime是我手动塞进 dict 的上下文时间”,容易误转 - 遇到嵌套对象(比如模型里有个
dict字段,里面又含datetime),default不递归处理,照样崩 - 一旦启用全局
app.json_encoder = MyEncoder,所有jsonify()都走这套逻辑,连你返回的纯字典、第三方库返回的对象也可能被意外重写 - 调试困难:错误堆栈里看不到具体哪一行触发了
default,只看到最终失败位置
真正需要它的场景极少,比如你明确只要把所有 Decimal 统一转 float,且确认没有其他复杂类型 —— 否则优先用前三种方式。
最麻烦的其实是混合查询和懒加载字段的组合:你以为只查了 id 和 name,结果某个字段的 getter 触发了关联表查询,还带着未初始化的 InstrumentedAttribute。这种时候,靠序列化层兜底不如在查询时就用 load_only() 或 defer() 显式控制字段加载范围。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










