JSON schema (v1)¶
The anchor save file is a single UTF-8 JSON document with indent=2, strict finite-number policy (allow_nan=False), and schema_version == 1. This page documents every field with types and validation rules.
The reference implementation lives in disklavier_visualizer/io/anchor_io.py. Tests in tests/test_anchor_io.py exercise round-trips, sort-on-save, and every rejection rule.
Example¶
{
"schema_version": 1,
"saved_at": "2026-05-08T15:42:08.317491+00:00",
"midi_path": "C:\\Users\\me\\study\\trial_001.mid",
"midi_duration_seconds": 187.402,
"anchors": [
{
"timestamp_seconds": 12.456,
"label": "phrase 1 opening"
},
{
"timestamp_seconds": 47.802,
"label": ""
},
{
"timestamp_seconds": 174.417,
"label": "first cord d"
}
]
}
Top level¶
| Field | Type | Required | Notes |
|---|---|---|---|
schema_version |
int | Yes | Must equal 1. Any other value raises AnchorParseError("Unsupported schema version: …"). |
saved_at |
string or null | No | ISO 8601 timestamp in UTC, written by save_anchors. Optional on load — if missing, becomes None in the runtime AnchorFile. Not validated as a date format. |
midi_path |
string | Yes | Absolute path to the MIDI file at save time. Not normalized on load. |
midi_duration_seconds |
float | Yes | Duration of the bound MIDI in seconds. Must be finite (NaN / Infinity rejected). Used at load time to detect drift. |
anchors |
array of Anchor | Yes | May be empty. |
Anchor¶
| Field | Type | Required | Notes |
|---|---|---|---|
timestamp_seconds |
float | Yes | Seconds from the start of the MIDI file. Must be finite. Out-of-range values (< 0 or > midi_duration_seconds) are clamped on UI load with a warning, but the JSON itself is not rejected. |
label |
string | No | Optional (defaults to "" on load if missing). Free text — UTF-8, no length limit, may contain whitespace. |
Validation rules¶
load_anchors enforces these checks. Any failure raises AnchorParseError with a specific reason:
- The file exists and can be opened.
- The content is valid JSON.
- The top-level value is an object (not an array, string, etc.).
schema_versionequals1.midi_pathis a string.midi_duration_secondsis convertible to a finitefloat.anchorsis a list.- For every anchor:
- The element is an object.
timestamp_secondsis convertible to a finitefloat.label, if present, is a string.
The validation is strict and non-recoverable — if any check fails, the load aborts and the previous in-memory anchors (if any) are preserved.
What is not checked at load time:
- That
midi_pathexists on disk. The UI checks separately and prompts to relocate. - That
midi_duration_seconds > 0. The on-disk format allows zero or negative durations; in practice they would never be saved. - That timestamps are within
[0, midi_duration_seconds]. The UI clamps on load with a status-bar warning rather than rejecting the file. - That timestamps are sorted. They are sorted on save, but the loader accepts any order (the UI re-sorts in
set_anchors).
Sort-on-save¶
save_anchors sorts the anchors by timestamp_seconds ascending for the on-disk copy only. The in-memory AnchorFile.anchors list is not reordered. The reason: re-saving the same session produces a stable diff (no spurious reordering of identical entries), but the UI ordering is not disturbed.
The runtime AnchorTableWidget keeps its anchors sorted independently anyway, so in normal UI flow the on-disk order matches the in-memory order.
Numeric encoding¶
allow_nan=Falseis passed tojson.dump. If a serializer attempts to writeNaN,Infinity, or-Infinity, it raisesValueErrorat save time. This surfaces in the UI asCould not save anchors.- The loader uses Python's standard
json.load, which by default acceptsNaN/Infinity(these are non-standard JSON extensions).load_anchorsthen explicitly checksmath.isfinite(...)on every numeric field — non-finite values are rejected withAnchorParseError.
So a hand-edited file with Infinity in it is rejected at load time, not silently accepted.
Path encoding on Windows¶
JSON requires backslashes inside strings to be escaped as \\. Python's json.dumps does this automatically:
"C:\Users\me\study\trial_001.mid" ← what a user might type
"C:\\Users\\me\\study\\trial_001.mid" ← what JSON requires
save_anchors always produces correctly-escaped output. When hand-editing on Windows, double-check that backslashes are doubled.
A test in test_anchor_io.py:88-94 verifies that Windows-style absolute paths round-trip with backslashes intact.
Editing the JSON by hand¶
Hand-editing is supported. The schema is intentionally flat — no hashes, no signatures, no checksums. The rules:
- Keep
schema_version == 1. - Keep every numeric field finite (no
NaN,Infinity,-Infinity). - Keep
midi_pathas a string. It can be an empty string, but the UI's path-existence check will then fail and prompt to relocate. - Keep every
anchor.timestamp_secondsfinite. Out-of-range values are clamped on UI load. - Keep
anchor.labelas a string (or omit it entirely — it defaults to"").
If you break a structural rule, loading the file raises AnchorParseError with a descriptive message, and your in-memory state is preserved.
What's deliberately not in the schema¶
- No anchor IDs. Anchors are identified by their position in the array (and that position changes when you add/remove others). This is fine because the schema does not support cross-references.
- No region anchors. Each anchor is a single timestamp. Region annotations (start + end) would need a v2 schema.
- No categories or colours. Labels are free text.
- No app version, no Python version, no machine ID. Only the data needed to reload the session is stored. The minimum delta between saves is the timestamp set +
saved_at.
Future schema versions¶
If the schema ever changes (e.g. adding region anchors), the version bump strategy is:
- Bump
SCHEMA_VERSIONinanchor_io.py. - Add a load-time migration path for
schema_version == 1files (read the v1 fields, populate sensible defaults for the new fields, return a v2-shapedAnchorFile). - Always save in the latest version.
Currently there is only v1, so the loader rejects anything else outright. Any v2 release would need to relax that check before accepting old files.