for domain tools

SJON is the DSL you don’t have to write.

Most domain tools eventually grow their own little config language. SJON saves you that work: a small embeddable data language with schemas, helpful diagnostics, units, comments, and clean JSON interop, ready to drop in. And it reads the same to the person reviewing the file as to the model that wrote it.

  • 1.0
  • CC0
  • Zig
  • JS
  • Rust

Take the tutorial Try it in your browser

/00 - why

What you get

Reach for SJON when JSON gets in the author's way but a full language would give them too much. You get the shared document machinery a homegrown DSL usually has to rebuild anyway: parsing, validation, formatting, diagnostics, editing, and interchange. All of it ready to plug into your tool.

files that read like the domain

The file looks like the thing it describes.

Each (name ...) group names the thing it describes. The :words inside are its fields. Units stay attached to their numbers, so authors aren't poking around nested objects wondering which field is which.

(camera
  :mode ortho
  :zoom 2
  :delay 4b)

errors that point at the typo

When something is off, your app can say so.

Schemas describe the forms and slots your app accepts. A misspelled key becomes a precise diagnostic with a span and a suggestion, not a vague parse failure.

error[unknown_key]:
(camera :mode ortho :zom 2)
                    ^^^^
note: Did you mean `:zoom`?

inline math, no runtime smuggled in

Small calculations stay small.

Expressions only call the functions you allow. Authors get inline math and helpers without imports, I/O, mutation, or recursion sneaking in along with them.

:zoom (* 2 (b 1))
:alpha (smoothstep 0 1 t)
:seed  (hash scene-name 4)

/01 - start

Start by reading the file

Get a feel for SJON the way an author writes it: named forms, keyword slots, units, comments, and small inline math. The same source text parses identically from Zig, JavaScript, or Rust, so your editor and your runtime agree.

Host-language starter snippets for Zig, JavaScript, and Rust.

Pick the host that fits your stack. The source text is the same either way. Just choose where you want to load it from.

const sjon = @import("sjon");

var tree = try sjon.parse(gpa, source);
defer tree.deinit();

const text = try sjon.print(gpa, tree, .{ .mode = .canonical });
defer text.deinit();

Prefer a guided path? Take the tutorial. Already wiring it up? Jump to README § Consuming or AUTHORING.md.

/02 - model

The whole model in 30 seconds

A SJON document is a tree built from a small set of pieces: forms, vectors, keywords, symbols, strings, booleans, nil, plain numbers, and numbers that carry a unit. The form below is small, but it touches most of that surface:

(camera :mode ortho :zoom 2 :delay 4b :pos [(* 2 1920) 1080])
  • Forms are constructors. (camera ...) is just data until a schema says what camera means and which fields it accepts.
  • Keywords are slots or flags. :mode ortho and :zoom 2 are key/value pairs; a lone :loop would be a positional flag.
  • Expressions are opt-in. (* 2 1920) only runs when the host has explicitly allowed *. Inside a vector it sits next to literals like any other value.
  • Units survive every hop. 4b stays a four-beat duration through printing, JSON, and binary IR. Same for 90deg, 50%, and 250ms.

/03 - compare

Why not just JSON or EDN?

JSON is still the best thing to send across the wire. SJON is what you reach for before that: the source file someone is actually editing, with comments, symbolic names, schema diagnostics, safe expressions, and lossless round trips.

See the side-by-side

/04 - agents

The other author

A growing share of config isn't typed by a person anymore — it's generated by a model and reviewed by one. SJON treats that author as first-class: hand it the schema, let it write the document, and the validator answers with something it can act on. Generate, validate, repair. The same loop the tutorial teaches humans runs fine without one.

a contract that fits in a context window

The schema is the prompt.

The schema is SJON too: one declaration that says what every form is allowed to be. Paste it into the prompt and the model has your domain's grammar — and it can't drift from the checker, because the checker reads the same bytes.

(form :name camera
  (key :name mode
    :type symbol)
  (key :name zoom
    :type number))
; the model reads what
; the checker enforces

errors a model can repair

The error is the instruction.

A hundred-plus stable diagnostic codes, each with a span and a semantic path. This isn't a hint for a human squinting at a terminal — it's an instruction an agent applies and re-validates, unsupervised.

error[unknown_key]:
(camera :mode ortho :zom 2)
                    ^^^^
note: Did you mean `:zoom`?

nothing to sandbox

Generated SJON is read, never executed.

Expressions call only the functions you allow — no imports, no I/O, no mutation, no recursion. Accepting machine-written input costs exactly what parsing data costs.

/05 - architecture

Parse once, share the tree

The parser builds one SoA Ast.Tree. Validation, printing, JSON, binary IR, structural edits, and expression evaluation all read from that same tree, so every tool agrees on spans, diagnostics, comments, and round trips by construction.

source.sjon Lexer Parser Ast.Tree Printer Validator Expr JSON Binary Edit
The parser runs once. Every downstream tool reads the same tree, so diagnostics, edits, and round trips can't drift apart.

Parsing SJON never turns a source file into a running program. It reads data. No imports, no I/O, no mutation, no recursion through user-defined code.

JSON stays the format you send to other systems. SJON keeps the distinctions your tools care about while someone is still editing the source: keyword vs symbol vs string, unit-bearing numbers, comments, spans, and original source order all the way through to binary IR.

The package itself stops at shared document machinery: parser, validator, printer, expression evaluator, editor reducer, JSON bridge, and binary reader. Your app stays in charge of what the words actually mean.

/06 - keep going

Keep going

Take the tutorial

Start with a single (scene ...) and grow it: atoms, units, expressions, schemas, cross-references, and fixing things by reading the diagnostic. One small file at a time, no setup required.

  1. 01 Orientation What SJON is, what it isn’t, how it differs from JSON.
  2. 02 Your first document Forms, keywords, vectors, and the canonical printer.
  3. 03 Atoms and intent Symbols, strings, numbers, booleans, nil. When to reach for which.
  4. 04 Numbers, units, vectors Unit suffixes, vector literals, when units are mandatory.
  5. 05 Forms and keyword pairing How keys parse, how positionals work, how to read a (form …).
  6. 06 Comments and strings Lossless comments, escape rules, raw strings.
  7. 07 Safe expressions Parens that compute. The closed expression vocabulary.
  8. 08 Bindings and control flow (let …), (if …), (cond …). Bounded, deterministic, no recursion.
  9. 09 Reading plugin schemas Forms, keys, required/optional, defaults, positional policy, open forms, schema export.
  10. 10 Discriminated and exclusive forms One head with variant shapes; exclusive groups; multi-key bundles.
  11. 11 Value kinds: shapes, vectors, units, bounds, representation Underlying shapes, vector shapes (fixed and variable length), unit shapes, numeric bounds, and representation tags.
  12. 12 Value kinds: strings, members, heads, unions, slot-local forms String bounds, member sets, head sets, unions, slot-local forms, and the diagnostic cheat sheet.
  13. 13 Cross-references Document-spanning name lookups; acyclic constraints.
  14. 14 Diagnostics-driven repair The stable diagnostic codes as a repair workflow. Read, repair, repeat.
  15. 15 Style, portability, capstone Canonical formatting, manifest portability, a capstone exercise.

Keep these tabs open

What to reach for while you wire SJON into a tool, sanity-check the 1.0 language surface, or look up anything from the examples on this page.