Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Feb 9, 2026

Auto-serialize EdgeSpec and NodeSpec Objects

This PR implements automatic serialization of EdgeSpec and NodeSpec objects to dictionaries, eliminating the need for users to manually call .to_dict().

Problem Fixed

Previously, users had to call .to_dict() on EdgeSpec/NodeSpec objects when passing them to ReactFlow:

# Old way - verbose and error-prone
flow = ReactFlow(
    edges=[
        EdgeSpec(id="e1", source="n1", target="n2").to_dict(),  # Required!
    ]
)

Without .to_dict(), the code would fail with:

TypeError: 'EdgeSpec' object is not subscriptable

Solution

Now EdgeSpec and NodeSpec objects are automatically converted to dictionaries:

# New way - automatic serialization!
flow = ReactFlow(
    edges=[
        EdgeSpec(id="e1", source="n1", target="n2"),  # No .to_dict() needed!
    ]
)

Implementation

  1. Added _normalize_nodes() and _normalize_edges() methods that convert Spec objects to dicts
  2. Registered param watchers to trigger normalization on assignment
  3. Added pre-normalization in __init__ for objects passed during initialization
  4. Prevented infinite recursion by checking if normalization is needed
  5. Used static methods correctly before super().__init__()

Changes

Code:

  • src/panel_reactflow/base.py: Implementation of auto-serialization
  • tests/test_api.py: Comprehensive test coverage

Documentation:

  • docs/how-to/define-nodes-edges.md: Updated guide with new feature

Testing

✅ All auto-serialization tests pass
✅ Original issue example works correctly
✅ No infinite recursion
✅ Backward compatible with existing dict-based code
✅ No security vulnerabilities detected

Benefits

  • Improved DX: No need to remember .to_dict()
  • Less boilerplate: Cleaner, more readable code
  • Backward compatible: Existing dict-based code continues to work
  • Type safety: Can use NodeSpec/EdgeSpec for IDE support
Original prompt

This section details on the original issue you should resolve

<issue_title>Autoserialize edges and nodes</issue_title>
<issue_description>I should not have to use .to_dict() on edges defined from EdgeSpec. This should happen automatically.

"""Example 1: Text Processing Pipeline — three nodes, no special dependencies."""

import panel as pn
import param
from panel.viewable import Viewer

from panel_reactflow import EdgeSpec, NodeSpec, NodeType, ReactFlow

pn.extension("jsoneditor")


class TransformNode(Viewer):
    text = param.String("Hello World")
    mode = param.Selector(default="upper", objects=["upper", "lower", "title", "swapcase"])

    @param.output(param.String)
    @param.depends("text", "mode")
    def result(self):
        if not self.text:
            return ""
        return getattr(self.text, self.mode)()

    def __panel__(self):
        return pn.pane.Markdown(self.result)


transform = TransformNode(name="Text Transform")


def create_pipeline(nodes, graph=None) -> ReactFlow:
    input_widgets = pn.Param(nodes[0], show_name=False)
    output_view = pn.panel(nodes[0])

    node_types = {"input": NodeType(type="input", label="Input", schema=type(nodes[0]))}

    flow = ReactFlow(
        nodes=[
            NodeSpec(id="input", position={"x": 0, "y": 100}, label=f"{input_widgets.name} Input").to_dict() | {"view": input_widgets},
            NodeSpec(id="transform", position={"x": 450, "y": 50}, label=f"{input_widgets.name}").to_dict() | {"view": output_view},
        ],
        edges=[
            EdgeSpec(
                id="e1",
                source="input",
                target="transform",
            ),  # .to_dict()
        ],
        node_types=node_types,
        sizing_mode="stretch_both",
        min_height=500,
    )
    return flow


create_pipeline(
    nodes=[transform],
).servable()
  File "/home/jovyan/repos/private/panel-reactflow/src/panel_reactflow/base.py", line 586, in _update_edge_editors
    edge_ids = [edge["id"] for edge in self.edges]
                ~~~~^^^^^^
TypeError: 'EdgeSpec' object is not subscriptable
```</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
<comment_new><author>@MarcSkovMadsen</author><body>
Its the same problem for Nodes.</body></comment_new>
</comments>

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits February 9, 2026 06:00
Co-authored-by: MarcSkovMadsen <42288570+MarcSkovMadsen@users.noreply.github.com>
…ds correctly

Co-authored-by: MarcSkovMadsen <42288570+MarcSkovMadsen@users.noreply.github.com>
Co-authored-by: MarcSkovMadsen <42288570+MarcSkovMadsen@users.noreply.github.com>
Copilot AI changed the title [WIP] Auto-serialize edges and nodes in EdgeSpec Auto-serialize EdgeSpec and NodeSpec to dictionaries Feb 9, 2026
Copilot AI requested a review from MarcSkovMadsen February 9, 2026 06:08
Copy link
Contributor

@MarcSkovMadsen MarcSkovMadsen left a comment

Choose a reason for hiding this comment

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

Local Claude and I am happy.

Manual Test App
"""Test app for verifying auto-serialization of NodeSpec/EdgeSpec.

This app demonstrates the new auto-serialization feature where NodeSpec
and EdgeSpec objects are passed directly to ReactFlow without calling
.to_dict() manually.
"""

import panel as pn

from panel_reactflow import EdgeSpec, NodeSpec, NodeType, ReactFlow

pn.extension("jsoneditor")

# Define a custom node type with a schema
node_types = {
    "task": NodeType(
        type="task",
        label="Task",
        schema={
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "enum": ["pending", "running", "done"],
                    "title": "Status",
                },
                "priority": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 5,
                    "title": "Priority",
                },
            },
        },
        inputs=["input"],
        outputs=["output"],
    ),
}

# Auto-serialization: pass NodeSpec objects directly, no .to_dict() needed
nodes = [
    NodeSpec(
        id="n1",
        type="task",
        position={"x": 50, "y": 50},
        label="Fetch Data",
        data={"status": "done", "priority": 3},
    ),
    NodeSpec(
        id="n2",
        type="task",
        position={"x": 300, "y": 50},
        label="Transform",
        data={"status": "running", "priority": 2},
    ),
    NodeSpec(
        id="n3",
        type="task",
        position={"x": 550, "y": 50},
        label="Load Results",
        data={"status": "pending", "priority": 1},
    ),
    # Mix: plain dict alongside NodeSpec objects
    {
        "id": "n4",
        "type": "panel",
        "position": {"x": 300, "y": 250},
        "label": "Plain dict node",
        "data": {},
    },
]

# Auto-serialization also works for EdgeSpec
edges = [
    EdgeSpec(id="e1", source="n1", target="n2", label="pipe"),
    EdgeSpec(id="e2", source="n2", target="n3", label="pipe"),
    EdgeSpec(id="e3", source="n2", target="n4"),
]

flow = ReactFlow(
    nodes=nodes,
    edges=edges,
    node_types=node_types,
    min_height=500,
    sizing_mode="stretch_both",
)

# Verify auto-serialization happened
for i, node in enumerate(flow.nodes):
    assert isinstance(node, dict), f"Node {i} should be a dict, got {type(node)}"
for i, edge in enumerate(flow.edges):
    assert isinstance(edge, dict), f"Edge {i} should be a dict, got {type(edge)}"

description = pn.pane.Markdown(
    """## Auto-Serialization Test

NodeSpec and EdgeSpec objects are passed **directly** to ReactFlow (no `.to_dict()` needed).

- **Nodes 1-3**: Created with `NodeSpec(...)` and passed directly
- **Node 4**: Created as a plain dict (still works)
- **Edges**: Created with `EdgeSpec(...)` and passed directly
- All nodes and edges are auto-converted to dicts internally
""",
    sizing_mode="stretch_width",
)

layout = pn.Column(description, flow, sizing_mode="stretch_both", min_height=700)
layout.servable()
Image


!!! note "Automatic Serialization"
`NodeSpec` and `EdgeSpec` objects are automatically converted to dictionaries
when passed to `ReactFlow`. You no longer need to call `.to_dict()` manually.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove the no longer. This framework is so new that almost no one would have read the documentation.

Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot . Please fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f343d26. Changed "You no longer need" to "You don't need" to avoid implying a previous state that users wouldn't be familiar with.

@MarcSkovMadsen MarcSkovMadsen marked this pull request as ready for review February 9, 2026 08:40
Co-authored-by: MarcSkovMadsen <42288570+MarcSkovMadsen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Autoserialize edges and nodes

2 participants