如何解决Python Flask中Decimal类型无法序列化JSON_自定义JSONEncoder

必须重写JSONEncoder,因jsonify依赖json.dumps默认不支持Decimal;需继承flask.json.JSONEncoder并重写default方法,对Decimal转float后交由父类处理,再通过app.json_encoder全局注册。

直接结论:必须重写 JSONEncoder,不能靠 int()float() 强转字段再传给 jsonify —— 那只是掩盖问题,且在精度丢失、负零、NaN 等边界场景会出错。

为什么 jsonify 会报 Object of type 'Decimal' is not JSON serializable

Flask 的 jsonify 内部调用的是 json.dumps(),而 Python 标准库的 json 模块默认只认 intfloatstr 等原生类型。decimal.Decimal 是独立实现的高精度类型,不在默认映射表里。一旦数据库查询返回含 Decimal 的元组或字典(比如 SELECT SUM(price)),jsonify 就会直接抛错。

常见错误现象:

  • 返回 HTTP 500,日志里只有 TypeError: Object of type 'Decimal' is not JSON serializable
  • 错误堆栈指向 flask/json/__init__.py 中的 dumps 调用
  • 仅在连接 MySQL/PostgreSQL 并使用原生驱动(如 pymysqlpsycopg2)时高频出现,SQLite 有时不暴露该问题(因部分驱动自动转 float

如何正确继承并注册 JSONEncoder(Flask 2.0+ 推荐方式)

从 Flask 2.0 开始,simplejson 已被移除,必须通过子类化 flask.json.JSONEncoder 并赋值给 app.json_encoder(旧版用 app.json_encoder,新版推荐 app.json.provider_class,但兼容写法仍有效)。

实操建议:

  • 定义一个新类,继承 flask.json.JSONEncoder,重写 default 方法
  • default 中只处理 decimal.Decimal,其余一律交还父类(super().default(o)),避免覆盖对 datetime 等其他类型的默认支持
  • 确保 return float(o) 而非 str(o) —— JSON number 类型需要数值,不是字符串;str(o) 会导致前端解析为字符串,破坏数值比较和计算逻辑
  • 注册时用 app.json_encoder = YourEncoder,不要漏掉这一步

示例代码:

Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

from flask import Flask
from flask.json import JSONEncoder
import decimal

class DecimalJSONEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return float(o)
        return super().default(o)

app = Flask(__name__)
app.json_encoder = DecimalJSONEncoder

为什么不用 json.dumps(..., cls=...) 手动序列化

你当然可以绕过 jsonify,自己调用 json.dumps(data, cls=DecimalEncoder) 再包装成 Response,但这会带来三个实际问题:

  • 丢失 jsonify 自动设置的 Content-Type: application/json 和状态码处理逻辑
  • 无法复用 Flask 对嵌套对象、Noneenum 等的默认编码行为,容易引发新错误
  • 团队协作中,每个接口都得显式写 cls=...,维护成本陡增;而全局注册一次 JSONEncoder 后,所有 jsonify 调用自动生效

更关键的是:如果你用了 Flask-RESTful、Flask-JWT-Extended 等扩展,它们内部也依赖 jsonify,手动 dumps 无法覆盖这些路径。

精度与兼容性要注意的细节

float(o) 看似简单,但有隐含风险:

  • Decimal('0.1')float 后是 0.10000000000000000555...,前端 JS 解析时可能显示为 0.10000000000000000555(尤其在金融类金额展示中不可接受)
  • 若需保留小数位(如金额统一两位),应在业务层做 o.quantize(decimal.Decimal('0.01')) 后再转 float,而不是在 JSONEncoder 里硬编码格式化逻辑
  • 某些老版本 Flask(json_encoder 属性名是 json_encoder,新版(2.2+)推荐用 app.json.provider_class,但直接设 json_encoder 在当前主流版本(2.0–2.3)仍完全可用

真正容易被忽略的点:这个修复只解决 Decimal,如果数据里同时混有 datetimebytes、自定义 model 实例,default 方法必须逐个判断并处理,否则仍会崩 —— JSONEncoder.default 是唯一出口,别指望它“自动兜底”。

相关推荐:

如何解决Python安装程序卡在预编译编译环节_关闭杀毒软件或清理缓存文件

pip卡在“Building wheel”或“Resolving packages…”主因是本地依赖解析或C++源码编译,非网络问题;关对进程、手动清缓存目录(如%LOCALAPPDATA%\pip\Cache或~/.cache/pip/Cache)、禁用杀软实时防护可解决多数情况。 Python安装程序卡在“Building wheel”或“Resolving packages…”不是网络慢,而...

Python类方法定义中必须显式声明self参数

Python类中定义的方法默认会自动接收实例对象作为第一个参数,若方法签名未包含self,调用时就会报“多传入1个位置参数”的错误——本质是Python隐式传入了实例,而函数未声明接收。 python类方法定义中必须显式声明`self`参数 在Python中,**所有实例方法(即定义在类内部、用于操作实例数据的方法)都必须将`self`作为第一个形参**。这不是约定俗成,而是语言强制要求:当通过实...

如何实现Python中线程安全的单例模式_使用元类metaclass或Lock加锁

因为__new__不是原子操作:线程A判断_instance为None后,未执行super().__new__前,线程B也判为None,导致重复创建;GIL不保护跨语句逻辑,需用双重检查锁定(DCL)加threading.Lock确保线程安全。 为什么直接用 __new__ 实现单例在多线程下会失效 因为 __new__ 本身不是原子操作:线程 A 判断 _instance 为 None 后,还没...

如何解决Python测试中数据库连接频繁开闭问题_利用fixture的yield模式

pytest fixture 中用 yield 管理 pymysql 连接更稳,因其确保无论测试是否异常都会执行清理;应避免全局连接池、多次 close、共享连接,并在 yield 前 ping 保活和重置状态。 频繁在 pytest 测试中用 pymysql.connect() 打开又关闭数据库连接,不是“看起来不优雅”的问题,而是会直接拖慢测试执行、触发 MySQL 的 max_connect...

Python中如何获取当前正在运行的所有异步任务_使用asyncio.all_tasks函数

asyncio.all_tasks 返回当前事件循环中所有未完成的Task对象集合(set),为空常因未启动事件循环、无活跃循环、任务已完结或跨线程未正确设置循环。 asyncio.all_tasks 返回什么,为什么有时为空 asyncio.all_tasks 返回当前事件循环中所有未完成的 Task 对象集合(set),但它**只作用于当前线程中已运行且未关闭的事件循环**。常见误区是直接调用...

Python怎么实现毫秒级定时任务_基于Apscheduler与Redis后端

APScheduler默认不支持毫秒级调度,因其IntervalTrigger最小单位为秒,传入小于1秒的seconds值会报ValueError;底层依赖time.sleep()和系统轮询,受CPython GIL及操作系统调度延迟影响,实际抖动达10–50ms以上。 为什么 APScheduler 默认不支持毫秒级调度 APScheduler 的 IntervalTrigger 最小单位是秒,...