Every domain tool eventually grows a little configuration language, and
then has to write the lexer, the parser, the formatter, and the error
messages to go with it. I wrote that machinery once, so you only have to
bring the vocabulary. The result reads the same to the person reviewing
the file as to the model that wrote it.
Config files start simple and then they stop being simple. You reach for JSON,
and within a month you want a comment saying why the timeout is 250 and not
200. You want 250ms to stay a duration instead of collapsing into a bare
number that some later reader has to guess the unit for. You want the file to
say ortho and have something tell the author when they type orhto. JSON
offers none of that, so the tool grows a language of its own, and that language
needs a lexer, a parser, a formatter, spans, and error messages. Every domain
tool ends up writing its own copy.
Here is the smallest SJON worth reading. It is also the first thing
the tutorial puts in front of you, and it carries
this whole page from here.
(camera:ortho:zoom2)
Read it as a tree:
(camera :ortho :zoom 2)
| | |
| | +-- kvpair: key `:zoom`, value `2`
| +--------- positional flag: the keyword `:ortho`, standing alone
+----------------- head: the symbol `camera`
Three parts, and each one earns its spelling. camera is the head, a bare
symbol, and it means nothing at all until a schema says what a camera is.
:zoom 2 is a kvpair, because a keyword followed by a value pairs with it.
:ortho is a flag, because a keyword followed by another keyword stands
alone. That rule is structural rather than lexical, which means the same
:ortho token is a flag here and would be a key in (camera :ortho 2). It is
the one piece of the syntax that catches people out, so hold on to it.
Every piece of machinery in SJON gets pointed at that camera, and the tutorial
grows it a lesson at a time. Here it is at the end, carrying all of them at
once.
;; The wide shot. 2 keeps the whole 1920 plate on screen.
(camera:name wide :projection ortho
:zoom2
:delay4b
:alpha (smoothstep01 t))
Nothing there is a new kind of thing. It is the same head, the same keywords,
the same numbers, with four claims attached:
Comments survive. The ;; line is captured as trivia hanging off the
form, so a lossless round-trip hands it back byte for byte. It is not a thing
the parser throws away and the formatter guesses at.
Units stay attached to their number.4b is four beats, not the number
4. It stays a duration through the printer, through the binary encoding, and
into JSON as {"$num": [4, "b"]}. Same for 90deg, 50%, and 250ms.
Expressions are opt-in and bounded.(smoothstep 0 1 t) runs only
because the host allowed smoothstep and supplied t. There are no imports,
no I/O, no mutation, and no recursion to allow.
:ortho turned into :projection ortho. I said the pairing rule was
structural. This is what that costs and what it buys. The flag was fine while
a camera was only ever orthographic; the moment there is a second projection,
the projection wants to be a key whose value gets checked, and changing the
spelling is the entire edit.
The camera above is still just shapes. What makes :projection ortho mean
something, and a misspelling of :zoom mean nothing, is a plugin: one
declaration of the forms and keys your application accepts.
scene.sjon
(plugin:name scene :version"1.0.0"
(value-kind:name projection :underlying symbol
:members (member-set:values [ortho perspective]))
(value-kind:name duration :underlying number
:unit (unit-shape:requiredtrue:allowed [s ms b]))
(form:name camera
(key:name name :type symbol)
(key:name projection :type projection)
(key:name zoom :type number :optionaltrue)
(key:name delay :type duration :optionaltrue)
(key:name alpha :type number :optionaltrue)))
Notice what that is written in. The schema is SJON, parsed by the same parser,
so there is no second grammar to learn and no way for the checker to drift from
the description. The duration kind is doing real work: it is why :delay 4b
is accepted and :delay 4 is not.
Four things ship with every diagnostic: a code, a severity, a source span, and
a semantic path saying where in the document you are. The help: line is a
page on this site, and there are 130 of them. The codes are
appended and never renamed, so a code you match on today keeps meaning the same
thing in the next release.
Before you scroll, try predicting one. Given the schema above, what does
(camera :name wide :projection isometric :zoom 2) report? The code is
not_member, and the message spells out the answer for you: expects `projection`, got `isometric` (allowed: `ortho`, `perspective`).
Every diagnostic codeAll 130 of them, each with a page. The same list your editor links to, and the same list the CLI prints.
The honest half. SJON is deliberately small, and some of what it will not do
for you is the point rather than a gap waiting to be filled.
It is not a programming language
No imports, no I/O, no mutation, no recursion, and no way for a document to
reach out and touch the world. Parsing a SJON file costs what parsing data
costs, which is what makes accepting a file you did not write a reasonable
thing to do.
It is not a wire format
JSON is still the best thing to send between systems, and SJON bridges to it
cleanly. SJON is what you reach for one step earlier: the source file somebody
is actually editing, where comments, units, and source order still matter.
It does not know your domain
The package stops at shared document machinery: parser, validator, printer,
expression evaluator, editor reducer, JSON bridge, binary reader. What
camera means, and what your program does with one, stays yours.
Why not just JSON or EDN?The long version of the three cards above, with the trade-offs laid out side by side.
The same source text parses identically from Zig, JavaScript, or Rust, so your
editor and your runtime never disagree about a file. Only the loading differs.
Those three agree because they are the same code. The Zig core compiles to
WebAssembly, and the Node and Rust hosts drive that module; a fourth host is a
native TypeScript port kept honest against the others. On every build a
conformance corpus replays across all four, which is the only reason I am
willing to say “identically” out loud. Two groups of cases are skipped on the
WebAssembly hosts, and each one is named with its reason beside the skip list
rather than quietly dropped.
The parser runs once and builds a single structure-of-arrays tree. Validation,
printing, JSON, binary encoding, structural edits, and expression evaluation
all read from that one tree.
Lexer and parser on the way in, one Ast.Tree in the middle, six consumers reading it.
That is not an implementation detail you can skip past, because it is what
holds the rest of this page together. The ;; comment on the camera is trivia
the parser hung on that form, which is why the lossless printer can put it back
exactly where it was and a structural edit cannot strand it. The :zom typo
had exactly one span, which is why the checker, the squiggle in your editor,
and the replacement an agent applies all point at the same four bytes. Two tools cannot disagree about a
document when there is only one copy of it.
A growing share of configuration is not typed by a person any more. It is
generated by a model and reviewed by one, and SJON is built for that author
too. Hand it the schema, let it write the document, and the validator answers
with something it can act on without a human in the loop. Point a model at
llms.txt to prime it on the whole language in a couple of thousand
tokens.
The schema is the prompt
The schema above is not a description of the format, it is the format. Paste it
into the prompt and the model has your domain’s grammar, and it cannot drift
from the checker, because the checker reads the same bytes.
The error is the instruction
unknown_key at camera/zom, span 16:3, replacement :zoom. That is not a
hint for a human squinting at a terminal. It is an edit an agent applies and
re-validates, and then the loop terminates because the codes are finite.
Generated SJON is read, never executed
The sandbox is the same one your own authors write in. A document a model wrote
cannot do anything a document you wrote could not do, which is a much shorter
security review than the alternative.
Fifteen lessons, all built on the camera you have already met. It starts with
that one form and grows it: atoms, units, expressions, schemas,
cross-references, and finally reading a diagnostic and repairing the file it
came from. One small file at a time, nothing to install.