Skip to content
D
Documentation

Text and serialization

concept
3 min readUpdated

Grafloria stores a diagram as structured data, then derives routing and pixels when a renderer displays it. Use JSON when Grafloria is the only reader and Mermaid-compatible text when people, Git, or Mermaid tools also need to read the document.

One model, two persistence paths

The engine owns the diagram data; a mounted renderer owns the live connection between that data and the canvas. A DiagramModel contains nodes, links, groups, strokes, and viewport data. A NodeModel is one of its typed, positioned nodes. Rendering and routing are not stored as pixels in the document.

mermaid
flowchart LR
  S[Structured diagram] --> J[JSON document]
  S --> M[Mermaid-compatible body]
  M --> C[Grafloria sidecar comments]
  J --> R[DiagramModel]
  C --> R
  R --> V[Rendered view]

Use DiagramSerializer for the JSON persistence path:

ts
import { DiagramSerializer, DiagramModel, NodeModel } from '@grafloria/engine';

const diagram = new DiagramModel('order-flow');
diagram.addNode(new NodeModel({
  id: 'plan',
  type: 'task',
  position: { x: 40, y: 80 },
  size: { width: 150, height: 64 },
}));
const serializer = new DiagramSerializer();
const flatDocument = serializer.serialize(diagram);
const json = JSON.stringify(flatDocument);

const parsedDocument: ReturnType<typeof serializer.serialize> = JSON.parse(json);
const restored = serializer.deserialize(parsedDocument);
const restoredNodes = restored.getNodes();

serialize() returns a plain object suitable for JSON encoding. deserialize() accepts that flat form and rebuilds the model. For new persistence, serializeEnvelope() returns a portable envelope with generator identity, creation time, and an integrity checksum; its deserialize() path verifies the checksum before loading.

Mermaid-compatible text

importDiagramText reads supported Mermaid forms into typed models. The supported forms are flowchart/graph, erDiagram, classDiagram, stateDiagram/stateDiagram-v2, architecture-beta, and block-beta. An unsupported recognised type is reported instead of being guessed as a flowchart.

ts
import { importDiagramText } from '@grafloria/engine';

const result = importDiagramText(`flowchart LR
  start[Start] --> gate{Gate}
  gate -->|yes| ship[Ship]
  gate -->|no| start`);

const nodes = result.diagram.getNodes();
const firstLabel = nodes[0]?.getData('label');
if (result.unsupported) {
  console.warn(`Unsupported Mermaid type: ${result.unsupported}`);
}

The importer assigns semantic types such as flowchart:process and flowchart:decision, so the resulting model retains diagram meaning rather than treating every box as a generic node.

Pure Mermaid text is the portable boundary. Its structure and labels can round-trip, but Mermaid has no standard syntax for all Grafloria positions, sizes, styles, ports, groups, or viewport state. Treat a pure-text import as best effort when those details matter.

The sidecar preserves Grafloria state

The live DiagramInstance returned by render exports Mermaid-compatible text with a lossless Grafloria sidecar by default. The visible body remains Mermaid; %%grafloria:document and %%grafloria:body-hash comments carry the serialized document and identify later body edits. Mermaid renderers ignore those comments.

ts
import { render } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';

const canvas = document.getElementById('canvas');
if (!(canvas instanceof HTMLElement)) {
  throw new Error('Missing #canvas element');
}
canvas.style.height = '400px';

const instance: DiagramInstance = render({
  nodes: [
    { id: 'plan', label: 'Plan', position: { x: 40, y: 80 }, size: { width: 150, height: 64 } },
    { id: 'ship', label: 'Ship', position: { x: 300, y: 80 }, size: { width: 150, height: 64 } },
  ],
  edges: [{ id: 'next', source: 'plan', target: 'ship' }],
}, canvas);

const text = instance.exportText();
const imported = instance.loadText(text);

console.log(imported.source); // 'sidecar'
console.log(instance.getModel().getNodes().length); // 2

The result is a mounted diagram with two connected nodes. Feeding the exported text to loadText() reconciles it into the existing instance, so its listeners, plugins, and renderer remain attached. Give the target a real height; otherwise the mounted canvas has no space to display the result:

css
#canvas {
  height: 400px;
}

When text has been edited

loadText() and importDiagramText() use prefer: 'auto' by default. An unchanged sidecar wins and restores the lossless document. If the body hash shows that someone edited the Mermaid body, the edited body wins; Grafloria reports bodyEdited: true. When a sidecar exists, the edited structure is applied over the sidecar so positions, styles, ports, groups, and viewport data that text cannot express survive; sidecarMerged reports this case.

Use prefer: 'sidecar' to discard body edits and load the saved Grafloria document, or prefer: 'text' to parse the body and ignore the sidecar:

ts
import { render } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';

const canvas = document.getElementById('canvas');
if (!(canvas instanceof HTMLElement)) {
  throw new Error('Missing #canvas element');
}
canvas.style.height = '400px';

const instance: DiagramInstance = render({
  nodes: [
    { id: 'plan', label: 'Plan', position: { x: 40, y: 80 }, size: { width: 150, height: 64 } },
    { id: 'ship', label: 'Ship', position: { x: 300, y: 80 }, size: { width: 150, height: 64 } },
  ],
  edges: [{ id: 'next', source: 'plan', target: 'ship' }],
}, canvas);

const saved = instance.exportText();
const edited = saved.replace('Plan', 'Draft');

const fromSavedDocument = instance.loadText(edited, { prefer: 'sidecar' });
const fromEditedBody = instance.loadText(edited, { prefer: 'text' });

console.log(fromSavedDocument.source); // 'sidecar'
console.log(fromEditedBody.source); // 'text'

The import result also reports sidecarInvalid when a sidecar line exists but its JSON cannot be parsed. In that case the body remains available as the fallback input. Check unsupported before treating an imported result as a usable diagram.

Choose the format

NeedUseWhat you get
Save and restore Grafloria stateDiagramSerializer.serializeEnvelope()A portable JSON envelope with integrity metadata
Exchange a diagram with people or Mermaid toolsinstance.exportText()Readable Mermaid-compatible text plus a lossless sidecar
Keep only portable textinstance.exportText({ lossless: false })Pure Mermaid text, with the documented lossy boundary
Edit text and keep the live canvasinstance.loadText(text)Reconciliation into the mounted instance
Import text without a mounted rendererimportDiagramText(text)An import result containing a DiagramModel and source diagnostics

JSON is the lossless Grafloria document format. Mermaid-compatible text is the human-readable format; retain its sidecar when a later Grafloria load must preserve the full document.

Was this page helpful?