From 2374290414d53f55748c1c79f3838330d678a54f Mon Sep 17 00:00:00 2001 From: Ernad Husremovic Date: Tue, 10 Mar 2026 13:56:35 +0100 Subject: [PATCH] fix: QwebJSON circular reference on non-serializable session_info values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The QwebJSON.dumps default handler returned non-serializable objects unchanged, causing Python's json encoder to report "Circular reference detected". Use odoo.tools.json.json_default() as fallback to properly convert ReadonlyDict, lazy, datetime, bytes, Domain and other types. 🤖 assisted by claude --- docs/QWEBJSON_CIRCULAR_REFERENCE.md | 73 +++++++++++++++++++ .../odoo/addons/base/models/ir_qweb.py | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 docs/QWEBJSON_CIRCULAR_REFERENCE.md diff --git a/docs/QWEBJSON_CIRCULAR_REFERENCE.md b/docs/QWEBJSON_CIRCULAR_REFERENCE.md new file mode 100644 index 00000000..cbfde564 --- /dev/null +++ b/docs/QWEBJSON_CIRCULAR_REFERENCE.md @@ -0,0 +1,73 @@ +# QwebJSON Circular Reference Bugfix + +## Problem + +When loading the Odoo 19 web client, the `web.webclient_bootstrap` QWeb template +fails with: + +``` +ValueError: Circular reference detected +``` + +at the line: + +```xml + +``` + +## Root Cause + +The `QwebJSON.dumps()` default handler in `odoo/addons/base/models/ir_qweb.py` +returned non-serializable objects unchanged: + +```python +# Original (broken) +class QwebJSON(json.JSON): + def dumps(self, *args, **kwargs): + prev_default = kwargs.pop('default', lambda obj: obj) + return super().dumps(*args, **kwargs, default=( + lambda obj: prev_default(str(obj) if isinstance(obj, QwebContent) else obj) + )) +``` + +When the JSON encoder encounters a non-serializable type (e.g. `ReadonlyDict`, +`lazy`, `datetime`, `bytes`, `Domain`), it calls the `default` handler. The handler +returns the object unchanged (since it's not `QwebContent`). The encoder tries to +serialize it again, gets the same object back, and raises "Circular reference detected." + +This is a **latent bug in the Odoo 19 core**. In vanilla Odoo it rarely triggers +because `session_info` values are typically JSON-serializable. However, any module +(OCA or custom) that introduces a non-serializable type into `session_info` or any +other QWeb JSON-serialized context will expose this bug. + +## Fix + +Use `odoo.tools.json.json_default()` as the fallback for non-QwebContent objects: + +```python +# Fixed +class QwebJSON(json.JSON): + def dumps(self, *args, **kwargs): + prev_default = kwargs.pop('default', lambda obj: obj) + return super().dumps(*args, **kwargs, default=( + lambda obj: prev_default(str(obj) if isinstance(obj, QwebContent) else json.json_default(obj)) + )) +``` + +`json_default()` (from `odoo/tools/json.py`) properly handles: + +- `datetime` -> string via `fields.Datetime.to_string()` +- `date` -> string via `fields.Date.to_string()` +- `lazy` -> resolved value +- `ReadonlyDict` -> regular `dict` +- `bytes` -> decoded string +- `Domain` -> list +- anything else -> `str(obj)` + +## File Changed + +- `odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_qweb.py` (class `QwebJSON`) + +## Date + +2026-03-10 diff --git a/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_qweb.py b/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_qweb.py index 0c642fc4..8f88f535 100644 --- a/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_qweb.py +++ b/odoo-bringout-oca-ocb-base/odoo/addons/base/models/ir_qweb.py @@ -656,7 +656,7 @@ class QwebJSON(json.JSON): def dumps(self, *args, **kwargs): prev_default = kwargs.pop('default', lambda obj: obj) return super().dumps(*args, **kwargs, default=( - lambda obj: prev_default(str(obj) if isinstance(obj, QwebContent) else obj) + lambda obj: prev_default(str(obj) if isinstance(obj, QwebContent) else json.json_default(obj)) ))