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.

  • 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.

(camera :ortho :zom 2)
                 ^ unknown_key
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 - 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.

/05 - 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.

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.