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

如何避免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))

关键修改点

  1. 递归层级传递:给to_json添加depth参数,默认值为1,每次递归调用时递增层级
  2. 动态绑定参数:用functools.partial把当前层级绑定到default函数,解决json.dumps无法传递额外参数的问题
  3. 解耦模型判断:通过DeclarativeMeta统一识别SQLAlchemy模型,避免硬编码类名(如object.__class__.__name__ == 'Author')
  4. 深度控制逻辑:当层级超过1时,不再递归序列化关联模型,返回<类名 id=xxx>的简略标识
  5. 修复关联配置:修正Book模型中back_populates的指向错误(原代码指向authors,应改为books)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 11:01:03