如何避免Flask JSON输出中多对多关系的递归问题?
解决ORM模型序列化的无限递归问题
报错信息
RecursionError: maximum recursion depth exceeded while calling a Python object
问题根源是ORM模型序列化时,关联关系存在反向引用(比如Book关联Author,Author又关联Book),导致序列化过程无限递归循环。
需求说明
以Author和Book的多对多关系为例:序列化时,若直接在default方法中调用关联模型的序列化方法,会引发递归;但硬编码指定关联对象的序列化字段会增加模型间耦合,因此希望通过跟踪递归层级来控制序列化深度:
- 首次调用(如
book.to_json)为第1层,正常序列化Book及其关联的Author的完整信息 - 当递归进入第2层(Author序列化其关联的Book),不再序列化Book关联的Author,而是返回简略标识(如
<Author id=xxx>)
期望的序列化结构
{ "id": 1, "name": "Python on Stack Overflow", "authors": [ { "id": 300, "name": "Mike", "books": [ { "id": 1, "name": "Python on Stack Overflow", "authors": ["<Author id=300>"] }, { "id": 2, "name": "The Worst Question Ever Asked", "authors": ["<Author id=100>", "<Author id=200>", "<Author id=300>", "<Author id=400>"] }, { "id": 3, "name": "The Greatest Question Ever Answered", "authors": ["<Author id=300>", "<Author id=400>"] } ] } ] }
当前模型代码
Book.py
# models/book.py def default(object): # 格式化日期 if isinstance(object, (date, datetime)): return object.strftime('%Y-%m-%d %H:%M %z') # 调用Author进行自序列化 if object.__class__.__name__ == 'Author': # <-- 可在此处添加层级判断;`and level==1` return object.to_json # 实例展示 return f'<{object.__class__.__name__} id={object.id}>' class Book(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text, index=True, unique=True, nullable=False) authors = db.relationship('Author', secondary=Published.__table__, back_populates='authors') @property def to_json(self): columns = self.keys() response = {} for column in columns: response[column] = getattr(self, column) return json.loads(json.dumps(response, default=default))
Author.py
# models/author.py def default(object): # 格式化日期 if isinstance(object, (date, datetime)): return object.strftime('%Y-%m-%d %H:%M %z') # 调用Book进行自序列化 if object.__class__.__name__ == 'Book': # <-- 可在此处添加层级判断;`and level==1` return object.to_json # 实例展示 return f'<{object.__class__.__name__} id={object.id}>' class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text, index=True, unique=True, nullable=False) books = db.relationship('Book', secondary=Published.__table__, back_populates='authors') @property def to_json(self): columns = self.keys() response = {} for column in columns: response[column] = getattr(self, column) return json.loads(json.dumps(response, default=default))
解决方案
通过传递递归层级参数,结合functools.partial绑定参数,实现序列化深度控制,同时降低模型间耦合:
修改后的Book.py
# models/book.py from functools import partial import json from datetime import date, datetime from sqlalchemy.ext.declarative import DeclarativeMeta def default(obj, depth=1): # 格式化日期类型 if isinstance(obj, (date, datetime)): return obj.strftime('%Y-%m-%d %H:%M %z') # 统一判断SQLAlchemy模型实例,避免硬编码类名 if isinstance(obj.__class__, DeclarativeMeta): # 控制递归深度:仅当当前层级<=1时,继续序列化关联模型 if depth <= 1: return obj.to_json(depth=depth + 1) # 超过层级后返回简略标识 return f'<{obj.__class__.__name__} id={obj.id}>' # 处理其他未知类型 return f'<{obj.__class__.__name__} id={obj.id}>' if hasattr(obj, 'id') else str(obj) class Book(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text, index=True, unique=True, nullable=False) # 修正back_populates指向错误,应对应Author的books字段 authors = db.relationship('Author', secondary=Published.__table__, back_populates='books') @property def to_json(self, depth=1): columns = self.keys() response = {col: getattr(self, col) for col in columns} # 使用partial绑定当前层级到default函数 custom_default = partial(default, depth=depth) return json.loads(json.dumps(response, default=custom_default))
修改后的Author.py
# models/author.py from functools import partial import json from datetime import date, datetime from sqlalchemy.ext.declarative import DeclarativeMeta def default(obj, depth=1): # 格式化日期类型 if isinstance(obj, (date, datetime)): return obj.strftime('%Y-%m-%d %H:%M %z') # 统一判断SQLAlchemy模型实例,避免硬编码类名 if isinstance(obj.__class__, DeclarativeMeta): # 控制递归深度:仅当当前层级<=1时,继续序列化关联模型 if depth <= 1: return obj.to_json(depth=depth + 1) # 超过层级后返回简略标识 return f'<{obj.__class__.__name__} id={obj.id}>' # 处理其他未知类型 return f'<{obj.__class__.__name__} id={obj.id}>' if hasattr(obj, 'id') else str(obj) class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text, index=True, unique=True, nullable=False) books = db.relationship('Book', secondary=Published.__table__, back_populates='authors') @property def to_json(self, depth=1): columns = self.keys() response = {col: getattr(self, col) for col in columns} # 使用partial绑定当前层级到default函数 custom_default = partial(default, depth=depth) return json.loads(json.dumps(response, default=custom_default))
关键修改点
- 递归层级传递:给
to_json添加depth参数,默认值为1,每次递归调用时递增层级 - 动态绑定参数:用
functools.partial把当前层级绑定到default函数,解决json.dumps无法传递额外参数的问题 - 解耦模型判断:通过
DeclarativeMeta统一识别SQLAlchemy模型,避免硬编码类名(如object.__class__.__name__ == 'Author') - 深度控制逻辑:当层级超过1时,不再递归序列化关联模型,返回
<类名 id=xxx>的简略标识 - 修复关联配置:修正Book模型中
back_populates的指向错误(原代码指向authors,应改为books)
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

