Skip to content

Python: recursively serialize nested container values - #7790

Open
wang ke (CoffeeDrivenCoder) wants to merge 3 commits into
microsoft:mainfrom
CoffeeDrivenCoder:coffeedrivencoder/fix-nested-serialization
Open

Python: recursively serialize nested container values#7790
wang ke (CoffeeDrivenCoder) wants to merge 3 commits into
microsoft:mainfrom
CoffeeDrivenCoder:coffeedrivencoder/fix-nested-serialization

Conversation

@CoffeeDrivenCoder

Copy link
Copy Markdown

Motivation & Context

SerializationMixin.to_dict() handled protocol objects only when they were direct list items or dictionary values. Protocol objects inside combinations such as dict -> list -> dict remained as Python objects, causing the corresponding to_json() call to fail with TypeError.

Description & Review Guide

  • What are the major changes? The existing scalar, protocol, list, dictionary, and unsupported-value branches now share a recursive serializer. Regression tests cover deeply nested protocol objects and preserve the existing behavior of skipping unsupported values.
  • What is the impact of these changes? Nested session and model state composed from supported serialization objects can be converted to dictionaries and JSON at arbitrary list/dictionary depth.
  • What do you want reviewers to focus on? Please review the recursive helper's preservation of existing exclusion and unsupported-value behavior.

Related Issue

Fixes #7788

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI balanced review requested due to automatic review settings August 20, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Aug 20, 2026
@CoffeeDrivenCoder

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@EsanRAHIMI Esan (EsanRAHIMI) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused change—the sentinel-based recursion preserves the existing skip behavior cleanly, and the nested protocol regression coverage exercises the original failure well.

One compatibility edge is worth confirming or pinning with tests: moving the (datetime, date, time) branch into the shared helper expands conversion beyond dictionary values. Top-level date/time attributes and date/time values inside lists were previously skipped as non-serializable, but are now emitted as strings. That may be a desirable consistency fix, but it is an observable behavior change beyond nested protocol serialization. Could you either confirm it is intentional and add coverage for those locations, or restrict the conversion to the prior dictionary-value context?

Keep date and time conversion scoped to dictionary values while retaining
recursive protocol and container serialization.
@CoffeeDrivenCoder

Copy link
Copy Markdown
Author

Esan (@EsanRAHIMI) You are right; the broader date/time conversion was not intentional. Addressed in 60d9e33 by moving (datetime, date, time) conversion back into the dictionary-value branch. Top-level date/time attributes and date/time list items are skipped as before, while dictionary values still serialize to strings and the nested protocol recursion remains intact. The regression test now covers all three date/time types in top-level, list-item, and dictionary-value locations. The 485 serialization/session/type tests, Ruff, source Pyright, and test Pyright all pass.

@EsanRAHIMI Esan (EsanRAHIMI) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up restores the previous date/time serialization boundary while keeping the nested container recursion intact. The regression test now covers datetime, date, and time across top-level attributes, list items, and dictionary values. This addresses my compatibility concern—thanks!

attribute_name=attribute_name,
)
if serialized is not _SKIP_SERIALIZATION:
serialized_dict[str(raw_key)] = serialized

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pin the intended behavior for nested non-string keys? Recursion changes nested {True: ..., None: ...} keys to "True" / "None"; previously to_dict() retained those keys and to_json() produced "true" / "null". It may be best to preserve the old output or add a test for the new normalization.

@CoffeeDrivenCoder wang ke (CoffeeDrivenCoder) Aug 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8f3b8be. The recursive helper now stringifies keys only for dictionaries that are direct object attributes, matching the previous to_dict() boundary. Keys in deeper dictionaries retain their original types, so to_json() again lets json.dumps() render True / None as "true" / "null". The regression test pins both the direct-key normalization and nested-key preservation.

if isinstance(value, list):
serialized_list: list[Any] = []
for item in cast(list[Any], value):
serialized = _serialize_value(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should recursive containers get cycle detection? A self-referential list or dict now raises RecursionError during to_dict(). Before this change, to_dict() returned and to_json() raised the controlled ValueError: Circular reference detected. I think it would be good to preserve a predictable failure and add a regression test before recursive user state reaches this path.

@CoffeeDrivenCoder wang ke (CoffeeDrivenCoder) Aug 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8f3b8be. Recursive list and dictionary traversal now tracks container IDs on the active recursion path and raises ValueError("Circular reference detected") on re-entry. IDs are removed in finally, so repeated acyclic references remain valid. Regression tests cover both self-referential lists and dictionaries. All 507 relevant serialization/session/type/workflow tests, Ruff lint/format, source Pyright, and test Pyright pass locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: SerializationMixin does not recursively serialize nested containers

4 participants