data — data & serialization
Mix reads and writes structured data through a single mental model: every
format parses to the same Value tree (string · number · bool · list · map ·
nil), and every encoder turns that tree back into text. There is one in-memory
shape; the format is just the doorway. So json_parse → walk with .field /
[i] → json_encode (or toml_encode, or data_encode) is the universal
round-trip.
List the category live with mix builtins json; one-line help for any name with
mix what NAME.
| Format | Text → Value | Value → text | File reader |
|---|---|---|---|
| JSON | json_parse | json_encode | read_json, read_jsonl |
| TOML | toml_parse | toml_encode | — (use read_file + toml_parse) |
| YAML | yaml_parse | yaml_encode | — (use read_file + yaml_parse) |
strict-data .mix | data_parse | data_encode | load_data |
| CSV | csv_parse | — | — |
| INI | ini_parse | — | — |
| XML | xml_parse | — | — |
| jq query | jq, jq_all (over a Value) | — | — |
| numeric coercion | to_number | — | — |
json_parse/json_encode, jq/jq_all, read_json/read_jsonl are gated
behind the json feature, toml_parse/toml_encode behind toml,
yaml_parse/yaml_encode behind yaml, and xml_parse behind xml; the shipped mix binary turns them all on.
data_parse/data_encode, load_data, csv_parse, ini_parse, to_number,
and template are core — always present, no feature gate.
This page also covers template
(placeholder substitution from a map) and the typed
sqlexec binds — the other two places Mix values
cross a text/SQL serialization boundary.
JSON
json_parse(string) turns a JSON document into a Mix value; field access is
.key for objects, [i] for arrays. json_encode(value[, pretty]) is the
inverse.
$m = json_parse("{\"name\":\"alpha\",\"port\":25,\"tags\":[\"a\",\"b\"]}")
print($m.name)
print($m.port)
print($m.tags[0])
alpha
25
a
$m = { name: "alpha", port: 25, on: true }
print(json_encode($m))
{"name":"alpha","on":true,"port":25}
Since 0.21 a Mix keyword works as a bare map key —
on: true,to: "x",label: 1all parse unquoted. The one exception isfn(it lexes identically tofunction): write{ "fn": … }. Keys with a dash/dot/space still need quotes. See maps.
A truthy second argument selects multi-line indented output:
$m = { name: "alpha", nested: { a: 1, b: 2 } }
print(json_encode($m, true))
{
"name": "alpha",
"nested": {
"a": 1,
"b": 2
}
}
Key ordering — the one JSON footgun
A Mix Map is insertion-ordered, but the JSON doorway is not — in either
direction. json_encode goes through serde_json without preserve_order,
so it emits keys sorted alphabetically; json_parse returns object keys
sorted the same way, not in document order. If byte-stable, order-preserving
output matters (a generated config a human will diff, a signed canonical form),
use data_encode instead — it
preserves insertion order.
$m = { zebra: 1, apple: 2, mango: 3 }
print("literal: " .. ("" .. $m)) -- map literal: insertion order
print("data_encode: " .. data_encode($m))
print("json_encode: " .. json_encode($m))
print("json_parse: " .. data_encode(json_parse("{\"zebra\":1,\"apple\":2}")))
literal: {zebra: 1, apple: 2, mango: 3}
data_encode: {"zebra": 1, "apple": 2, "mango": 3}
json_encode: {"apple":2,"mango":3,"zebra":1}
json_parse: {"apple": 2, "zebra": 1}
JSON value mapping
| JSON | Mix |
|---|---|
null | nil |
true / false | bool |
| number | Number (f64) |
| string | String |
| array | List |
| object | Map (IndexMap) |
$m = json_parse("{\"a\":null,\"b\":3.5,\"c\":1000000000000}")
print($m.a == nil)
print($m.b)
print($m.c)
true
3.5
1000000000000
Mix numbers are f64, so an integer above 2^53 loses exact precision on the way through — a documented constraint, not a bug (see numbers). JSON object keys must be strings, and they come back sorted, not in document order (§above).
What JSON can't represent
json_encode is loud on non-finite numbers: a NaN / ±inf anywhere in
the value raises a catchable error (JSON has no spelling for them — encoding
0 silently would corrupt data):
try
print(json_encode({ bad: sqrt(-1) }))
catch $e
print("" .. $e)
end
json_encode: non-finite number NaN has no JSON representation (JSON has no NaN/Infinity)
A function or bytes value inside the tree encodes as null — JSON
simply has no such type:
print(json_encode({ f: function($x) return $x end, s: "ok" }))
{"f":null,"s":"ok"}
data_encode is stricter on both counts — it raises on functions and bytes
too (§strict-data below).
Reading JSON from disk
read_json($path) is read_file + json_parse in one call (one record per
file). read_jsonl($path[, opts]) reads a JSON-lines file — one independent
record per line — into a list, skipping blank lines.
-- /tmp/rec.json = {"name":"alpha","port":25,"tags":["a","b"]}
$r = read_json("/tmp/rec.json")
print($r.name)
print($r.tags[1])
alpha
b
-- /tmp/recs.jsonl =
-- {"id":1,"ok":true}
-- {"id":2,"ok":false}
-- {"id":3,"ok":true}
$recs = read_jsonl("/tmp/recs.jsonl")
print(length($recs))
print($recs[0].id)
print($recs[2].ok)
3
1
true
read_jsonl is strict by default — one malformed line aborts the whole read
with a line number. Pass { skip_errors: true } to silently drop bad lines
(useful for a log whose rotated tail can be truncated):
-- /tmp/bad.jsonl has a junk middle line
$strict = read_jsonl("/tmp/bad.jsonl") -- raises: "... line 2: ..."
$lenient = read_jsonl("/tmp/bad.jsonl", { skip_errors: true })
print(length($lenient)) -- the two good records
2
Parse errors raise
json_parse / read_json / read_jsonl raise a catchable runtime error on
malformed input (the same fail-fast policy as the rest of Mix — a broken literal
is a script bug, not absent data). Wrap with try/catch:
try
$m = json_parse("{oops")
catch $e
print("bad json: " .. ("" .. $e))
end
bad json: json_parse: key must be a string at line 1 column 2
jq — querying a Value
jq(value, filter) and jq_all(value, filter) run a real jq
filter (embedded jaq engine) over any Mix value
— typically json_parse output, but any list/map tree works. The value is a
Mix value, not a JSON string: json_parse is the text→value door, jq is the
query door — any list/map literal works too, no JSON round-trip needed. Design
notes: the jq-builtin design doc (operator control repo since 2026-07-23).
The split is about output cardinality, the thing jq makes hard for a single-return language:
jq(value, filter)— for a filter that yields 0 or 1 output. 0 →nil, 1 → that value unwrapped. A filter that returns an array as one value (map(.x),[ .a[] ]) belongs here — it's one output. More than one output raises.jq_all(value, filter)— for a stream filter (.items[],.[] | select(…)). Always a list: 0 outputs →[], N →[v0, …]. You can alwaysfor eachit.
$m = json_parse("{\"a\":{\"b\":{\"c\":42}}}")
print(jq($m, ".a.b.c"))
42
$m = json_parse("{\"items\":[{\"n\":\"x\"},{\"n\":\"y\"}]}")
print(jq($m, "[ .items[].n ]")) -- bracketed: one array output -> jq()
[x, y]
$m = json_parse("{\"items\":[{\"n\":\"x\",\"sz\":50},{\"n\":\"y\",\"sz\":150}]}")
$big = jq_all($m, ".items[] | select(.sz > 100) | .n") -- stream -> jq_all()
print($big)
[y]
A stream filter handed to jq() raises with the fix spelled out:
$m = json_parse("[1,2,3]")
print(jq($m, ".[]"))
Runtime error at line 2: jq(): filter produced more than one output; use jq_all(), or wrap the filter as `[ … ]` / `map(...)` / add `first` to make it a single value
Absent data vs. errors
A valid filter over absent data is not an error — .missing → jq null →
Mix nil for jq(), and an empty stream → [] for jq_all(). There is no
default argument; use jq's own alternative operator //:
$m = json_parse("{\"a\":1}")
print(jq($m, ".missing // \"none\"")) -- absent -> the alternative
$r = jq($m, ".nope")
if $r == nil then print("got nil") end
none
got nil
A malformed filter or a jq runtime type error (e.g. indexing a number)
raises, like json_parse on bad JSON:
$m = json_parse("{\"a\":1}")
print(jq($m, ".a |"))
Runtime error at line 2: jq(): cannot parse filter ".a |": Parse([(Term, "")])
jq (the embedded jaq engine) preserves object key insertion order —
jq($m, ".")returns keys in their original order, and constructed objects likejq($m, "{b: .b, a: .a}")keep the written order. The alphabetical look in JSON examples above comes fromjson_parse/json_encode, not from jq. For canonical, order-stable serialization reach fordata_encode.
YAML (v0.71.0)
yaml_parse(string[, {docs: true}]) → value; yaml_encode(value) → a YAML
string. This is the surface the provisioned-YAML fleet runs on — Grafana
alerting/dashboards/datasources, Prometheus and Alloy config that deploy
scripts generate, edit, and gate on: a deploy script that cannot parse
what it deploys cannot validate it either (this used to shell out to
ruby -ryaml).
$y = yaml_parse(read_file("rules.yaml"))
for each $g in $y["groups"]
print($g["name"] .. ": " .. len($g["rules"]) .. " rule(s)")
end
write_file("rules.yaml", yaml_encode($y))
The mapping mirrors JSON's: scalars → nil/bool/number/string, sequences → lists, mappings → maps in document order. The details that differ from a naive reading:
- Documents. By default the input must hold at most one document —
none is
nil, more than one raises (naming the fix). Pass{docs: true}for a list of every document in the stream. - Keys. Mix maps are string-keyed: scalar YAML keys keep their natural
text (
25:becomes"25"); a sequence or mapping used as a key raises. - Anchors and aliases are resolved during parsing; tags are not
preserved. Merge keys are NOT resolved: yaml-rust2 has no
<<handling, so<<: *defaultsparses as a literal"<<"entry whose inherited fields are not flattened into the map — a gating script must flatten them itself or avoid<<in files it validates..inf/.nan-style reals that Rust cannot parse keep their text as strings rather than being guessed at. - Encoding emits no leading
---marker, guarantees a trailing newline, writes whole numbers as integers, and quotes only what YAML requires — including keys likeon/yesthat YAML 1.1 would read as booleans.nil/bool/number/string/list/map only; bytes, buffer or function values raise — and so does a non-finite number (bareinf/NaNwould read back as strings;json_encodeandtoml_encoderefuse the same way). - Nesting is capped at 256 levels in both directions (a hostile
deep-nesting input raises a catchable error, like
serde_json's 128). - Round trip holds:
yaml_parse(yaml_encode($v))rebuilds the same value.
TOML
toml_parse(string) → map; toml_encode(value) → a pretty TOML string. Tables
become nested maps; a whole-valued number encodes as a TOML integer, a fractional
one as a float.
$t = toml_parse("name = \"alpha\"\nport = 25\n\n[server]\nhost = \"192.0.2.1\"")
print($t.name)
print($t.port)
print($t.server.host)
alpha
25
192.0.2.1
$m = { name: "alpha", port: 25, server: { host: "192.0.2.1" } }
print(toml_encode($m))
name = "alpha"
port = 25
[server]
host = "192.0.2.1"
There is no read_toml; compose it — toml_parse(read_file($path)). TOML
datetimes parse to a string.
TOML has no representation for Mix nil, function, bytes, or buffer
values. toml_encode therefore raises TOML_UNREPRESENTABLE instead of
silently replacing any of them with "". The structured error's details
map carries the exact path and type, including list indexes:
try
print(toml_encode({jobs: [{payload: freeze(buffer([1, 2]))}]}))
catch $message, $error
print($error.code)
print($error.details.path)
print($error.details.type)
end
TOML_UNREPRESENTABLE
$.jobs[0].payload
bytes
Convert deliberately before encoding: base64_encode($bytes) for binary
payloads, omit an absent field rather than storing nil, and never put a
function in a data tree.
strict-data .mix — the substrate format
Mix has its own JSON-equivalent data format: a .conf.mix / .spec.mix file is a
JSON-shaped tree with a friendlier surface — bareword keys, optional top-level
braces, # comments, trailing commas, nil. Crucially it parses only as
data: no $vars, no $(cmd), no ${...}, no function calls, no control flow.
It is the format for substrate-internal config that must never run as code.
data_parse(string)— text → value (the non-executing parser).data_encode(value[, pretty])— value → text, with correct escaping; round-trips throughdata_parseand preserves key order.load_data($path)— read +data_parsea file (the non-executing twin ofsource/include, which run a file).
Literal heredocs are accepted as strings, including their normal trailing-newline
semantics. A heredoc containing ${...} interpolation or $(...) command
substitution is rejected like every other executable strict-data construct.
$cfg = { name: "alpha", port: 25, tags: ["a", "b"] }
print(data_encode($cfg))
print(data_encode($cfg, true)) -- truthy 2nd arg = multi-line
{"name": "alpha", "port": 25, "tags": ["a", "b"]}
{
"name": "alpha",
"port": 25,
"tags": [
"a",
"b"
]
}
data_encode emits exactly the escaping the strict-data lexer round-trips —
\$, \", \\, \n, \t, \r, \e; any other control character as
\u{…}; and a leading \~ when (and only when) the string is ~ or starts
with ~/, the two shapes the lexer would expand to $HOME — so a string that
ends in $ (a regex) or contains a backslash survives intact. This is what
makes it safe to splice hostile values
into a generated config or a remote script: the value arrives as inert data, never
as a command. (It is not a shell escaper — don't feed its output to /bin/sh.)
$v = { rx: "ends-with$", win: "a\\b" }
print(data_encode($v))
{"rx": "ends-with\$", "win": "a\\b"}
The round-trip is exact for any data-shaped tree:
$orig = { name: "alpha", regex: "x$", tags: ["a", "b"], n: 7 }
$back = data_parse(data_encode($orig))
print($back.regex)
print($back.tags[1])
print($back.n)
x$
b
7
data_parse accepts the canonical no-braces top-level map (newline- or
comma-separated key: value):
$d = data_parse("name: \"alpha\"\nport: 25\nenabled: true")
print($d.name)
print($d.enabled)
alpha
true
Strict-data does not accept executable Mix's ; statement separator — use
newlines or commas. data_parse("a: 1; b: 2") raises a line-numbered
strict-data violation rather than quietly widening the configuration format.
A missing comma inside a braced map or list is named as exactly that
(0.72.0): missing `,` after this map entry, anchored at the line of the
entry that lacks it. (It used to surface as expected RBrace, got String("b") at the line after the omission — the parser's state, not the
author's mistake, and 20 lines away in a real config.)
…and rejects every executable construct with a line-numbered error — that rejection is the whole point:
print(data_parse("x: now()"))
Runtime error at line 1: data_parse: Strict-data violation at line 1: function call `now(...)` not allowed in data files. data files cannot invoke functions; use a literal value
Keys that are Mix keywords need no quoting (since 0.21): a .conf.mix key
named to, on, in, or label parses bare. The one exception is fn — it
lexes identically to function, so quote it ("fn": 1).
load_data is the file-reading form, for data the Mix tooling and a Rust
consumer should read through the one same parser (e.g. a mesh inventory):
-- /tmp/inv.mix (strict-data, NOT a script):
-- name: "mesh-inventory"
-- nodes: [
-- { host: "node1", addr: "192.0.2.1" },
-- { host: "node2", addr: "192.0.2.2" },
-- ]
$inv = load_data("/tmp/inv.mix")
print($inv.name)
print($inv.nodes[0].host)
print($inv.nodes[1].addr)
mesh-inventory
node1
192.0.2.2
Every way load_data can fail is catchable (since 0.36.1) — missing file,
unreadable file, syntax error in the data, or an executable construct the
strict-data rules reject. All four arrive as ordinary runtime errors naming the
path, so a try/catch around a config read actually holds:
$cfg = nil
try
$cfg = load_data("/etc/cosmix/app.conf.mix")
catch $e
print("cannot read config: " .. $e)
exit(2)
end
Before 0.36.1 a malformed file raised a raw parse error, and
try/catchdeliberately does not catch those — the script aborted with exit 1 straight through the handler above, while a missing file was caught normally. If you rely on thatcatch, checkmix --version.
data_encodecannot represent a non-finite number (NaN/±inf), a function value, or a bytes value — it raises a catchable error rather than emit silently-lossy text (wherejson_encodedegrades functions/bytes tonull, §above).
CSV & INI — text tables and config
csv_parse(string[, delim]) reads the first line as a header and returns a
list of maps keyed by those headers. Fields are trimmed. The optional second
argument is a one-character delimiter (default ,).
$rows = csv_parse("name,port\nalpha,25\nbeta,143")
print($rows[0].name)
print($rows[1].port)
print(length($rows))
alpha
143
2
$rows = csv_parse("a|b\n1|2", "|")
print($rows[0].a)
print($rows[0].b)
1
2
Three sharp edges: the parser is a naive split — no RFC-4180 quote handling,
so a quoted field containing the delimiter splits in two ("x,y" becomes the
fields "x and y"); only the first character of the delimiter argument is
used; and a row with more fields than headers keys the extras by 0-based field
index (col1, col2, …). Blank lines are skipped.
ini_parse(string) reads [section] headers and key = value lines into a
nested map. Lines starting # or ; are comments. Keys before any section land
under _global.
$ini = ini_parse("debug = true\n[server]\nhost = 192.0.2.1\nport = 25\n[log]\nlevel = info")
print($ini["_global"].debug)
print($ini.server.host)
print($ini.server.port)
print($ini.log.level)
true
192.0.2.1
25
info
Every CSV / INI value is a string — even
25andtrue. These parsers do no type inference (a value with leading zeros, a hostname, and a port look the same as text). Coerce numerics yourself withto_number(next section). JSON, TOML, and strict-data preserve the types they can represent; each serializer documents what happens when a Mix-only type reaches its boundary. TOML and strict-data raise rather than substituting a different value.
XML — SOAP/RSS documents
xml_parse(string[, {mode}]) parses strict XML into a Value tree. The
default simple mode is shaped for consuming SOAP/RSS-style API responses:
namespace prefixes are stripped, attributes become @name keys, repeated
sibling elements collapse into a list, a leaf element's trimmed text becomes
its value, mixed text lands under #text, and xmlns declarations are
dropped. The result is {RootName: …}, so a SOAP response navigates as plain
map access:
$r = xml_parse("<e:Envelope xmlns:e=\"urn:x\"><e:Body><status>OK</status><ns><s>ns1.example.net</s><s>ns2.example.net</s></ns></e:Body></e:Envelope>")
print($r.Envelope.Body.status)
print($r.Envelope.Body.ns.s[1])
print(length($r.Envelope.Body.ns.s))
OK
ns2.example.net
2
Attribute and mixed-text keys start with @/#, so they need bracket access:
$a = xml_parse("<a id=\"7\">hi & bye</a>")
print($a.a["@id"])
print($a.a["#text"])
7
hi & bye
Pass {mode: "tree"} for full fidelity: every element is a
{name, attrs, children} node with namespace prefixes and xmlns attributes
preserved and child order intact; text children appear as plain strings
(whitespace-only text between elements is dropped in both modes).
$t = xml_parse("<ns:a x=\"1\">t<b/>u</ns:a>", {mode: "tree"})
print($t.name)
print($t.attrs.x)
print($t.children[0])
print($t.children[1].name)
ns:a
1
t
b
Sharp edges: strict XML only — real-world HTML is tag soup and will not
parse (that needs an HTML5 parser, deliberately not this builtin). Every parsed
value is a string (like CSV/INI — coerce with to_number). Simple mode is
lossy by design: ordering across differently-named siblings, mixed-content
interleaving, and namespace identity are all dropped — reach for {mode: "tree"} when they matter. A single child element is a scalar and two-plus
collapse to a list, so a maybe-repeated element needs a type() check (or
navigate the tree mode instead). Only predefined (& < >
' ") and numeric character entities resolve — no DTD, so custom
entities error. Input must be UTF-8 (a bytes payload from http_get is
accepted and decoded strictly). Nesting is capped at 256 levels; malformed
input, mismatched/unclosed tags, multiple roots, and text outside the root all
error rather than best-guess.
to_number — numeric coercion
to_number(value) returns the value as a Number, or nil if it can't coerce.
It accepts a number, a numeric string (surrounding whitespace trimmed), or a bool
(true→1, false→0); anything else → nil. This is the bridge from
string-typed CSV/INI/form data back to arithmetic.
print(to_number("3.14"))
print(to_number(" 42 "))
print(to_number(true))
print(to_number("nope"))
print(to_number([1, 2]))
3.14
42
1
nil
nil
Coercion is strict about non-finite spellings: "inf", "infinity",
"nan", and an overflowing "1e999" all return nil — a word the Rust float
parser happens to accept is not a Mix numeric string. is_number agrees on the
string case. Number values still propagate IEEE-754 non-finites through
math (sqrt(-1) is NaN) — it is the string doorway that's strict. Ordinary
scientific notation is fine (to_number("1e6") → 1000000). See
numbers.
print(to_number("inf"))
print(to_number("1e999"))
print(is_number("nan"))
print(sqrt(-1))
nil
nil
false
NaN
The nil-on-failure return is the validation idiom — branch instead of crashing:
$rows = csv_parse("name,port\nalpha,25")
$p = to_number($rows[0].port)
if $p == nil then
print("port is not a number")
else
print($p + 1)
end
26
template — filling text from a map
template(tmpl, map) substitutes single-brace {key} placeholders (not
{{key}}) with the map's values, stringified the way print would. The second
argument must be a map — anything else raises.
print(template("{name} is a {role}", { name: "alpha", role: "relay" }))
print(template("{n} items, ok={ok}", { n: 3, ok: true }))
alpha is a relay
3 items, ok=true
Three rules make it safe over untrusted values:
- Single-pass: a substituted value is emitted verbatim and never rescanned, so data containing
{other_key}cannot inject a second substitution — and the output never depends on map iteration order:
print(template("{a} and {b}", { a: "{b}", b: "SECRET" }))
{b} and SECRET
- A
{key}not present in the map, a{with no closing}, and a nested{all stay literal — no error, no silent empty string. - Matching is byte-exact — no Unicode normalization, no case-folding (see strings).
sqlexec — typed SQL binds
The embedded SQLite builtins (sqlite feature; on in the shipped binary) are
the other place Mix values cross a serialization boundary. sqlopen(path)
opens read-only; sqlopen(path, "rw") opens read-write (WAL mode, 5s busy
timeout). sqlexec(handle, sql[, params]) executes with ? placeholders;
sqlclose(handle) closes.
Parameters bind typed — never stringified:
| Mix value | SQLite bind |
|---|---|
nil | NULL |
| bool | INTEGER 0/1 |
| whole finite number (i64 range) | INTEGER |
| any other number (fractional, huge) | REAL |
| string | TEXT |
| bytes | BLOB |
| list / map / function | loud error — no SQL representation |
params is a list (one element per ?) or a single bare value. A non-reading
statement (INSERT/UPDATE/CREATE …) returns {affected: N}; a
row-returning statement (SELECT, a row-returning PRAGMA — decided by SQLite
itself, not a keyword match) returns a list of maps, mapping back NULL →
nil, INTEGER/REAL → number, TEXT → string, and a BLOB → the
placeholder string "<blob N bytes>".
$db = sqlopen(":memory:", "rw")
sqlexec($db, "create table t (a, b, c)")
print(data_encode(sqlexec($db, "insert into t values (?, ?, ?)", [nil, true, 2.5])))
print(data_encode(sqlexec($db, "select typeof(a) as ta, typeof(b) as tb, typeof(c) as tc from t")))
sqlclose($db)
{"affected": 1}
[{"ta": "null", "tb": "integer", "tc": "real"}]
Binds are the exact-bytes path — prefer them over composing SQL text with
sql_quote (which escapes for a string literal and doubles a backslash on the
way into SQLite; see system).
Boundary validation (v0.29.0)
Mix's tolerant nil semantics stay the default everywhere — a missing map key
reads as nil, and general string coercion renders that as "nil", which is
exactly how a blank provisioning field once became part of a constructed
hostname. The validation family makes strictness a one-call choice at your
job/API/form boundary; failures raise structured VALIDATION_* errors with
details {path, expected, actual_type} (see errors):
$job = validate($raw, {
node: {type: "string", nonblank: true},
host: {type: "string", nonblank: true},
vmid: {type: "integer", min: 100, max: 999999},
plan: {enum: ["gold", "silver"]},
tags: {required: false, type: "list", items: {type: "string", nonblank: true}},
owner: {required: false, type: "map", schema: {name: {nonblank: true}}}
})
validate(value, spec)— validates a map against a field spec, returns the ORIGINAL map unchanged (composable; no hidden normalization). Rules per field:required(default TRUE),type(one name or a list —any nil bool number integer string bytes buffer list map function;integer= finite whole within ±2^53-1),nonblank,enum(normal Mix equality, so"8080"matches8080),min/max(inclusive numeric),min_length/max_length(string codepoints / list items / map entries),items(rule map for every list element),schema(nested field spec). Optional fields that are absent or nil skip their rules; unknown INPUT fields pass through; a typo'd RULE key raisesVALIDATION_SPECinstead of silently no-opping. Violation paths readowner.name,tags[2].require_key(map, key)— assert present + non-nil, return the value (VALIDATION_REQUIRED).expect_type(value, kind)— assert the type, return the value (VALIDATION_TYPE).nonblank(value[, label])— assert a non-blank string, return it UNTRIMMED; the label names the value in the error (VALIDATION_NONBLANK).get_or(map, key, default)— the tolerant twin: default covers both absent and nil.
Using validate(...)/require_key(...) as a bare statement is a legitimate
assertion — the raise is the point; the return value is for composing.
Choosing a format
- JSON — interop with the outside world (HTTP APIs, JMAP, other tools). Loses key order in both directions (keys come back sorted); types preserved.
- strict-data
.mix(data_*/load_data) — substrate-internal config and data a human authors or an agent generates. Order-preserving, comment-friendly, safe-by-construction (never runs as code), exact round-trip. The default for anything Mix owns end-to-end. - TOML — interop with TOML-native tooling.
- CSV / INI — ingesting legacy text tables / config; string-typed, no encoder.
- jq — querying/filtering a value you already have, especially predicate selection over a stream.
See also
- strings — string vs. raw quoting,
${...}interpolation,..concat - collections — lists, maps, indexing,
.field/[i]access - functions — and the HOFs (
map/filter/sort_by) you run over parsed data - files —
read_file/write_file/read_lines/read_file_bytes - errors —
try/catcharound a parse that may raise - modules —
source/include(which run a file) vs.load_data(which doesn't) - Bus messaging —
data_encodefor safely splicing values into a remote script - math — numeric builtins over the numbers
to_numberproduces - numbers — f64 precision, radix literals, non-finite propagation
- system —
sql_quote/shell_quoteand when binds beat quoting - The mix repo
mix builtins json list every JSON / TOML / data builtin
mix what NAME one-line description of a single builtin (e.g. mix what jq)
mix help the full categorized builtin reference