Project a cyclic dataclass graph without calling asdict recursively
A cyclic dataclass graph needs an application-defined projection before serialization. dataclasses.asdict() recursively converts dataclass fields and nested containers, but it has no cycle-marker policy. When a Node points to itself through child, the attempted asdict(root) follows that relationship until Python raises RecursionError. The example catches the failure solely to contrast it with a controlled diagnostic projection.
The replacement tracks private object identities in seen during the traversal. On revisiting an identity, it emits the public value "<cycle>"; numeric object IDs are never included in output. The separate max_depth condition handles an acyclic graph that is deeper than the desired diagnostic representation. Removing an identity in finally is important: a later sibling may legitimately reference the same object without being a cycle on that sibling’s current path.
This particular projector intentionally preserves only name and child. It is not a general replacement for asdict, a deep-copy promise, or a JSON schema. Select fields, cycle markers, and depth policy for the application that consumes the projection. The example requires Python 3.7+ because dataclasses and asdict were introduced then.
AI assistance disclosure: this article was drafted with AI using one in-memory synthetic node.
Source: Python dataclasses documentation.
from dataclasses import asdict, dataclass
@dataclass
class Node:
name: str
child: object = None
def project(node, seen=None, depth=0, max_depth=3):
if seen is None:
seen = set()
if node is None:
return None
if id(node) in seen:
return "<cycle>"
if depth >= max_depth:
return "<max-depth>"
seen.add(id(node))
try:
return {
"name": node.name,
"child": project(node.child, seen, depth + 1, max_depth),
}
finally:
seen.remove(id(node))
root = Node("root")
root.child = root
try:
asdict(root)
except RecursionError:
print("asdict raises RecursionError")
projection = project(root)
assert projection == {"name": "root", "child": "<cycle>"}
print(projection)
asdict raises RecursionError
{'name': 'root', 'child': '<cycle>'}