Sketch JSON (`.sketchjson`)
Policies that keep the org coherent
Cross-repo interchange contract for Sketch Studio and Novolis.Avalonia.Controls.Sketch.
| Extension | .sketchjson |
| Implementation | `SketchJson.cs` |
| Product host | Sketch Studio |
| Dogfood | SketchLab |
| Related CAD | cadjson.md (different domain — do not mix) |
This page is the wire contract. There is no separate JSON Schema file; the C# DTO + round-trip tests are authoritative.
Conventions
- UTF-8 JSON, camelCase, indented (
WriteIndented) System.Text.JsonwithJsonStringEnumConverter(camelCase enum names)DefaultIgnoreCondition = WhenWritingNull— defaults omitted on write; readers apply defaults- 2D points:
{ "x": number, "y": number }in authoring / world units (screen-style Y down as drawn) - Stable string element
ids (GUID"N"hex when generated) - Not persisted: selection, undo/redo stacks, viewport camera, tool, UI chrome
Versions
version | Meaning |
|---|---|
1 | Legacy strokes only; missing kind → stroke; no rotation/groups/text/images expected |
2 | kind, rotationDegrees, groupId, text fields, imagePngBase64 |
3 | Layers: layers[], activeLayerId, element layerId |
≤ 0 | Treated as 1 on load |
Hosts write 3 when non-default layers are present. Old files continue to load.
Document root
{
"version": 3,
"grid": {
"size": 20,
"visible": true,
"snapEnabled": true
},
"activeLayerId": "layer-default",
"layers": [
{ "id": "layer-default", "name": "Layer 1" }
],
"elements": []
}| Field | Type | Default / notes |
|---|---|---|
version | number | See table above |
layers | array? | { id, name, visible?, locked? } |
activeLayerId | string? | Active layer for new elements |
grid.size | number | 20 |
grid.visible | boolean | true |
grid.snapEnabled | boolean | false if omitted |
elements | array | Ordered z-order (first = bottom) |
Element object
Every element is a StrokeShape-shaped record:
| Field | Type | Write | Load default |
|---|---|---|---|
id | string | always | new GUID if missing |
kind | enum string | omit when stroke | stroke |
strokeColor | string | #RRGGBB or #AARRGGBB | #1e1e1e |
strokeWidth | number | 2 if ≤ 0 | |
fillColor | string? | omit if empty (#RRGGBB / #AARRGGBB) | null |
strokeStyle | enum string | omit when solid | solid |
closed | boolean? | omit when false | false |
rotationDegrees | number? | omit when ≈ 0 | 0 |
groupId | string? | omit when none | null |
layerId | string? | omit when default layer | default layer |
text | string? | omit when empty | null |
fontSize | number? | omit when default 16 for text kinds | 16 |
imagePngBase64 | string? | images only | null |
points | {x,y}[] | geometry | [] |
kind enum
| Value | Geometry |
|---|---|
stroke | Polyline / freehand / box / ellipse / speech-bubble outline |
text | points[0] = text anchor; text + fontSize |
textBox | points define placement rect; border + text |
image | points define placement rect; imagePngBase64 holds PNG |
strokeStyle enum
solid (default), dashed, dotted, dashDot, stipple
Fuse / groups
Ctrl+G (Fuse) assigns the same groupId to every selected element. Selecting any member expands to the whole group for move/resize/rotate/delete. Ctrl+Shift+G clears groupId.
Rotation
rotationDegrees is about the axis-aligned bounding box center of points (before rotation). Hit-testing and export apply the same transform.
Full example (v2)
{
"version": 2,
"grid": {
"size": 20,
"visible": true,
"snapEnabled": true
},
"elements": [
{
"id": "a1b2c3d4e5f64789a0b1c2d3e4f50617",
"strokeColor": "#1e1e1e",
"strokeWidth": 2,
"closed": true,
"fillColor": "#2a9d8f",
"points": [
{ "x": 40, "y": 40 },
{ "x": 120, "y": 40 },
{ "x": 120, "y": 100 },
{ "x": 40, "y": 100 },
{ "x": 40, "y": 40 }
]
},
{
"id": "b2c3d4e5f6478901a2b3c4d5e6f70819",
"kind": "text",
"strokeColor": "#e63946",
"text": "Hello",
"fontSize": 22,
"rotationDegrees": 15,
"points": [
{ "x": 50, "y": 120 },
{ "x": 120, "y": 150 }
]
},
{
"id": "c3d4e5f64789012a3b4c5d6e7f8091a0",
"kind": "image",
"imagePngBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"points": [
{ "x": 200, "y": 40 },
{ "x": 280, "y": 40 },
{ "x": 280, "y": 120 },
{ "x": 200, "y": 120 },
{ "x": 200, "y": 40 }
]
}
]
}Legacy v1 load
{
"version": 1,
"grid": { "size": 20, "visible": true, "snapEnabled": false },
"elements": [
{
"id": "legacy",
"strokeColor": "#000000",
"strokeWidth": 2,
"points": [ { "x": 1, "y": 2 }, { "x": 3, "y": 4 } ]
}
]
}Loads as kind: stroke, rotationDegrees: 0.
What is not in the file
| Concern | Where it lives |
|---|---|
| PNG / SVG export | Sketch Studio SketchExport (clipboard / Save As) |
| MRU / last path | %LocalAppData%\Novolis\Sketch Studio\settings.json |
| Active tool / UI | Session only |
| Selection / undo | Session only |
Tests & hosts
- Unit:
Novolis.Avalonia.Unit—SketchDocumentTests(gridify, undo, fuse, v1/v2 round-trip) - Smoke:
dotnet run --project d:\novolis\novolis-apps\src\SketchStudio -- --smoke - Host documentation tree: Sketch Studio docs/