Serialization
JSON-based diagram persistence with DrawingManager.ToDto() and LoadFromDto()
Key Methods: ToDto(), LoadFromDto() — DTO Types: DiagramDto, ComponentDto, LineDto
Table of Contents
Serialization Flow
The serialization system transforms the live diagram state into a portable JSON representation and back again. The process uses plain Data Transfer Objects (DTOs) serialized via System.Text.Json, ensuring no dependency on Newtonsoft.Json or binary formatters.
The DiagramDto is the root container:
DiagramDto Structure
namespace Beep.Skia.Serialization
{
public class DiagramDto
{
public const int CurrentSchemaVersion = 2;
public int SchemaVersion { get; set; } = CurrentSchemaVersion;
public string ThemeName { get; set; }
public List<ComponentDto> Components { get; set; } = new();
public List<LineDto> Lines { get; set; } = new();
}
}
Component Serialization
Each SkiaComponent on the canvas is serialized into a ComponentDto. The DTO captures the minimum information needed to reconstruct the component: its position, size, type identity, configurable properties, and connection-point GUIDs:
ComponentDto Structure
public class ComponentDto
{
public string Type { get; set; } // assembly-qualified type name
public float X { get; set; }
public float Y { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public string Name { get; set; }
// Lightweight string values
public Dictionary<string, string> PropertyBag { get; set; } = new();
// Typed values (lists, dictionaries, nested objects) as raw JSON elements
public Dictionary<string, JsonElement> TypedPropertyBag { get; set; } = new();
// Persisted connection point IDs for deterministic identity across sessions
public List<Guid> InPointIds { get; set; } = new();
public List<Guid> OutPointIds { get; set; } = new();
}
Property serialization splits values into two bags: simple values that survive a string round-trip go into PropertyBag, while complex values are stored as JsonElement entries in TypedPropertyBag so they survive deserialization intact.
ToDto() — Serializing Components
var dto = new DiagramDto
{
ThemeName = theme.Name
};
foreach (var component in drawingManager.GetComponents())
{
var cDto = new ComponentDto
{
Type = component.GetType().AssemblyQualifiedName,
X = component.X,
Y = component.Y,
Width = component.Width,
Height = component.Height,
Name = component.Name
};
foreach (var kvp in component.NodeProperties)
{
// Store simple values in the string bag; complex values in the typed bag
if (kvp.Value.ParameterCurrentValue is string s)
cDto.PropertyBag[kvp.Key] = s;
else
cDto.TypedPropertyBag[kvp.Key] =
JsonSerializer.SerializeToElement(kvp.Value.ParameterCurrentValue);
}
cDto.InPointIds.AddRange(component.InConnectionPoints.Select(p => p.Id));
cDto.OutPointIds.AddRange(component.OutConnectionPoints.Select(p => p.Id));
dto.Components.Add(cDto);
}
var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("diagram.json", json);
Connection Serialization
Connection lines are serialized by reference — they store the GUIDs of their start and end ConnectionPoint IDs rather than embedding the full port data. During deserialization, a GUID-to-port lookup index resolves these references:
LineDto Structure
public class LineDto
{
public Guid StartPointId { get; set; }
public Guid EndPointId { get; set; }
// Used by templates, before GUIDs exist
public int StartComponentIndex { get; set; } = -1;
public int EndComponentIndex { get; set; } = -1;
public bool ShowStartArrow { get; set; } = true;
public bool ShowEndArrow { get; set; } = true;
public string Label1 { get; set; }
public string Label2 { get; set; }
public string Label3 { get; set; }
public string DataTypeLabel { get; set; }
// State-machine transition semantics
public string GuardCondition { get; set; }
public string TransitionAction { get; set; }
public string TriggerEvent { get; set; }
public uint LineColor { get; set; } // SKColor as RGBA uint
public int RoutingMode { get; set; } // LineRoutingMode
public int FlowDirection { get; set; } // DataFlowDirection
public float ArrowSize { get; set; }
public float[] DashPattern { get; set; }
public int StartMultiplicity { get; set; } // ERDMultiplicity
public int EndMultiplicity { get; set; }
public string SchemaJson { get; set; }
public string ExpectedSchemaJson { get; set; }
}
Enum values are stored as integers and colors as RGBA uint values, keeping the JSON compact and round-trip safe.
Deserialization
Loading a diagram from JSON involves several coordinated steps to reconstruct the full object graph:
- Parse JSON: The JSON string is deserialized into a
DiagramDtousingSystem.Text.Json. - Clear and theme:
LoadFromDtoclears the current components and applies the persistedThemeName. - Look Up Types: Each
ComponentDto.Typeis an assembly-qualified name resolved withType.GetType(); unknown types are skipped. - Instantiate Components: Components are created with
Activator.CreateInstance(), positioned, and named from the DTO. - Restore Property Values:
PropertyBagandTypedPropertyBagentries are applied throughSetPropperties()(which also updates the component'sNodePropertiesmetadata). - Restore Port Identity: Persisted
InPointIds/OutPointIdsare re-applied to the component's connection points so line references stay stable. - Restore Connections: For each
LineDto, ports are resolved by GUID (or by component index for templates) and reconnected.
Saving and Loading a Diagram
// Save
var dto = drawingManager.ToDto();
var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("diagram.json", json);
// Load
var restored = JsonSerializer.Deserialize<DiagramDto>(File.ReadAllText("diagram.json"));
drawingManager.LoadFromDto(restored);
drawingManager.RequestRedraw();
Versioning
The SchemaVersion property in DiagramDto is an int that identifies the payload shape. DiagramDto.CurrentSchemaVersion is currently 2 and is written on every save:
| Version | Changes | Compatibility |
|---|---|---|
1 | Initial format. Components with position, size and type, plus a flat property bag. | Loads; complex property values fall back to string conversion. |
2 | Added TypedPropertyBag for complex values and persisted connection-point IDs (InPointIds/OutPointIds). | Current. Full feature parity. |
Loading is tolerant by design: missing bags and port-ID lists are treated as empty, and any property that cannot be applied is skipped rather than failing the load. Check SchemaVersion yourself if you need to run custom upgrade logic for payloads you produced outside the framework:
Version Check on Load
var dto = JsonSerializer.Deserialize<DiagramDto>(json);
if (dto.SchemaVersion < DiagramDto.CurrentSchemaVersion)
{
// Optional: migrate legacy payloads before loading
UpgradeLegacyDto(dto);
}
drawingManager.LoadFromDto(dto);
Note
Serialized files always carry DiagramDto.CurrentSchemaVersion. LoadFromDto reads both bags and tolerates older payloads, so the migration hook above is only needed for custom transformations.
Complete Save/Load Example
Below is a complete end-to-end example demonstrating saving a diagram to a file and loading it back:
Saving a Diagram
using System.Text.Json;
var drawingManager = new DrawingManager();
var start = new ProcessNode { Name = "Start" };
start.X = 100; start.Y = 100;
drawingManager.AddComponent(start);
var process = new ProcessNode { Name = "Process Data" };
process.X = 400; process.Y = 100;
drawingManager.AddComponent(process);
var decision = new DecisionNode { Label = "Valid?" };
decision.X = 250; decision.Y = 300;
drawingManager.AddComponent(decision);
drawingManager.ConnectComponents(start, process);
drawingManager.ConnectComponents(process, decision);
var dto = drawingManager.ToDto();
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string json = JsonSerializer.Serialize(dto, options);
File.WriteAllText("workflow_diagram.json", json);
Console.WriteLine($"Saved diagram with {dto.Components.Count} components " +
$"and {dto.Lines.Count} lines.");
Loading a Diagram
string json = File.ReadAllText("workflow_diagram.json");
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var dto = JsonSerializer.Deserialize<DiagramDto>(json, options);
var drawingManager = new DrawingManager();
drawingManager.LoadFromDto(dto);
drawingManager.RequestRedraw();
Console.WriteLine($"Loaded: {drawingManager.GetComponents().Count} components, " +
$"{drawingManager.GetLines().Count} lines");