Novolis Docs
novolis-governance / sketchjson.md

Sketch JSON (`.sketchjson`)

dotnetgovernancenovolis

Cross-repo interchange contract for Sketch Studio and Novolis.Avalonia.Controls.Sketch.

Extension.sketchjson
Implementation`SketchJson.cs`
Product hostSketch Studio
DogfoodSketchLab
Related CADcadjson.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.Json with JsonStringEnumConverter (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

versionMeaning
1Legacy strokes only; missing kind → stroke; no rotation/groups/text/images expected
2kind, rotationDegrees, groupId, text fields, imagePngBase64
3Layers: layers[], activeLayerId, element layerId
≤ 0Treated 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": []
}
FieldTypeDefault / notes
versionnumberSee table above
layersarray?{ id, name, visible?, locked? }
activeLayerIdstring?Active layer for new elements
grid.sizenumber20
grid.visiblebooleantrue
grid.snapEnabledbooleanfalse if omitted
elementsarrayOrdered z-order (first = bottom)

Element object

Every element is a StrokeShape-shaped record:

FieldTypeWriteLoad default
idstringalwaysnew GUID if missing
kindenum stringomit when strokestroke
strokeColorstring#RRGGBB or #AARRGGBB#1e1e1e
strokeWidthnumber2 if ≤ 0
fillColorstring?omit if empty (#RRGGBB / #AARRGGBB)null
strokeStyleenum stringomit when solidsolid
closedboolean?omit when falsefalse
rotationDegreesnumber?omit when ≈ 00
groupIdstring?omit when nonenull
layerIdstring?omit when default layerdefault layer
textstring?omit when emptynull
fontSizenumber?omit when default 16 for text kinds16
imagePngBase64string?images onlynull
points{x,y}[]geometry[]

kind enum

ValueGeometry
strokePolyline / freehand / box / ellipse / speech-bubble outline
textpoints[0] = text anchor; text + fontSize
textBoxpoints define placement rect; border + text
imagepoints 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

ConcernWhere it lives
PNG / SVG exportSketch Studio SketchExport (clipboard / Save As)
MRU / last path%LocalAppData%\Novolis\Sketch Studio\settings.json
Active tool / UISession only
Selection / undoSession 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/