Workflow manager: Transition update returns 500 error due to Expression object not being JSON serializable

Hello everyone,

While testing the workflow manager locally, I encountered an issue when editing and saving transitions. I wanted to confirm whether this is a known problem or unintended behavior before opening a formal issue or preparing a fix.

What I observed

When editing a transition (for example changing the title or another field) and clicking Save changes, the frontend sends a PATCH request to the transitions endpoint: “/@transitions/{workflow_id}/{transition_id}”

The request is sent correctly, but the backend responds with: 500 Internal Server Error

From the backend logs, the error is: “TypeError: Object of type Expression is not JSON serializable”

The traceback indicates that the failure occurs while rendering the JSON response in plone.restapi, suggesting that a workflow object (likely a guard or condition expression) is being returned without being converted to a JSON-serializable format.

Why this seems problematic

Because of this error:

  • Transition edits cannot be reliably saved from the UI.

  • The user receives a server error instead of a structured response.

  • Workflow editing becomes difficult during normal use.

Since transition editing is a core feature, this appears to affect standard workflows rather than an edge case.


My current understanding (please correct me if I’m mistaken)

It looks like the transition serializer or response builder may be returning a DCWorkflow object (such as an Expression) directly, and this is not converted to a JSON-safe value before calling json.dumps().

Hi Devraj – thank you for looking into this. You don’t say which version of Plone and Volto you’re using, but if we assume they are the current ones and you are using a new, not custom site to test with, it does seem that you have found a bug. Does it happen with any transition on any workflow?

Hi @tkimnguyen, thank you for your response.

I tested this on a fresh local setup, and the issue occurs consistently.

Environment

  • Plone: 6.1.1

  • Volto: 18.22.0

  • Fresh site, no customizations

  • OS: Ubuntu 22.04 (WSL)

What I observed:

When updating a transition (even changing only the title), the request: “ PATCH /@transitions/{workflow_id}/{transition_id} “
returns: 500 Internal Server Error TypeError: Object of type Expression is not JSON serializable

Workflow and state updates work correctly, so the issue appears specific to transitions.

Root cause (based on local debugging):

After tracing the code, I found that DCWorkflow stores certain guard-related properties (permissions, roles, groups, and expr) as Expression objects.

In serialize_transition() (in backend/src/workflow/manager/api/services/workflow/transition.py), these objects were being returned directly in the API response. During response rendering, json.dumps() fails because Expression objects are not JSON serializable.

Fix tested locally: I implemented a small fix locally:

  • Added a helper function to safely extract string values from Expression objects.

  • Updated serialize_transition() so that guard-related fields are converted to JSON-safe values before being returned.

After applying this change:

  • Transition updates work correctly.

  • The API returns a valid JSON response.

  • Changes persist after reload.

If this approach seems reasonable, I can prepare a PR with:

  • The serialization fix

  • Basic tests to ensure transition updates return valid JSON

Please let me know if you would like me to proceed with this direction or if there are preferred conventions for handling Expression fields in serializers here.

Thanks again for taking a look.

Thanks Devraj – I’m not familiar with the code but @manaskng is. Perhaps he can chime in.

@tkimnguyen I recently noticed that a similar issue had already been raised, but the description wasn’t very clear. I’ve resolved the issue and submitted a PR, and everything is working fine locally. I’m currently waiting for a review. However, it seems there may not be anyone actively reviewing PRs in the workflow-manager repository. Could you please assign someone to review? I’ve also noticed a few more bugs that I plan to work on.

Hi Devraj – I’m really interested in having workflow manager polished and ready to use by mortals :grin: so thank you for your interest and energy! In the Plone community we don’t assign things to people since we are all volunteers. Let me see who I can find who would be willing and able to help review your PRs.

Hi @devrajpardhi04 , I oversee the new workflow manager for volto. Thanks for pointing this out, I’ve fixed the issue along with some minor bugs I found.
I’ve closed your PR since the approach isn’t the right one for this.

Hi @manaskng ,

I was re-checking the transition serialization flow in workflow-manager repo after the recent changes, and I noticed that workflow transitions are still breaking in some cases after a guard update.

The current approach seems to assume that guard values are plain strings

From what I observed, the main issue is that we can’t always assume guard values are plain strings. After UpdateTransition, fields like guard.permissions, roles, groups, and sometimes guard.expr are stored as Expression objects (e.g. Expression('string:...')) rather than raw strings.

In the current approach, these values are passed directly into the response dict. That works only when the guard still contains strings (legacy/default state), but once a transition is updated through the API, the serializer ends up returning Expression objects, which later causes JSON serialization errors (Object of type Expression is not JSON serializable).

That’s why I introduced the helper function not as extra logic, but as a normalization step before building the response. The idea is simply to convert internal DCWorkflow representations into stable API-safe types (strings/lists of strings), so the response shape stays consistent regardless of whether guard contains strings or Expressions internally.

From what I tested, this makes PATCH transition responses stable and prevents the 500 error after guard updates.

I might be missing some context though do you think there’s a cleaner way to normalize guard values without a helper, or is there another preferred pattern in this codebase? If you have a better direction, I’d love to align with it.

(Added temporary debug logs inside serialize_transition() just to inspect runtime values.)

From the debug output, I can see that after a guard update some transitions contain:

  • guard.permissionsExpression objects

  • guard.exprExpression objects

while transitions with empty/default guards contain plain values.

This confirms that guard data is stored internally as Expression after updates, which then reaches the API response and causes JSON serialization to fail.