Standard library / primitives
JSON, XML & TOML
Two builtins, round-trippable: for JSON-shaped values (maps, lists, text, numbers, bools, nothing) json_decode(json_encode(x)) reconstructs x — big integers included. Plus JSON Lines, xml_parse and TOML.
-- Doc example: JSON encode/decode (pure, round-trippable).
intent: "doc example: JSON"
print(json_encode({"name": "Ada", "tags": ["x", "y"], "n": 3})) -- run shows the JSON
-- The message of an error, or nothing when the call did not fail.
task error_of(f)
try
f()
recover e
give e
give nothing
test "xml_parse follows the xmltodict convention: @attributes, repeated children as a list (engine v0.6.20+)"
let x be xml_parse("<order id=\"7\"><item sku=\"a\"/><item sku=\"b\"/><note>hi</note></order>")
assert_eq(keys(x), ["order"])
assert_eq(x["order"]["@id"], "7")
assert_eq(length(x["order"]["item"]), 2)
assert_eq(x["order"]["item"][1]["@sku"], "b")
assert_eq(x["order"]["note"], "hi") -- text-only element → its text
test "toml_parse / toml_encode: tables → maps, arrays → lists, dates → ISO text (engine v0.6.20+)"
let t be toml_parse("title = 'demo'\n[server]\nport = 8080\nhosts = ['a', 'b']\nborn = 2026-09-11\n[[items]]\nname = 'x'\n[[items]]\nname = 'y'\n")
assert_eq(t["title"], "demo")
assert_eq(t["server"]["port"] + 1, 8081) -- a number, not text
assert_eq(t["server"]["hosts"], ["a", "b"])
assert_eq(t["server"]["born"], "2026-09-11") -- TOML dates come back as ISO text
assert_eq(length(t["items"]), 2)
let back be toml_parse(toml_encode({"name": "demo", "n": 3, "db": {"host": "x"}}))
assert_eq(back["db"]["host"], "x")
test "encode then decode reconstructs the value"
let data be {"name": "Ada", "tags": ["x", "y"], "n": 3}
let s be json_encode(data)
let back be json_decode(s)
assert_eq(back["name"], "Ada")
assert_eq(back["n"], 3)
assert_eq(length(back["tags"]), 2)
test "special values: a secret is an error, bytes base64, decimal exact, nothing null, dates as ISO text"
assert_eq(json_encode({"b": bytes("Hi"), "d": 1.50d, "n": nothing}), "{\"b\": \"SGk=\", \"d\": 1.50, \"n\": null}")
-- engine v0.6.46+: a secret is never written as text, also deep inside (it was "[redacted]")
assert(contains(error_of(() => json_encode({"auth": {"key": as_secret("hunter2", "PW")}})), "the value[\"auth\"][\"key\"] is secret(PW)"))
assert(contains(error_of(() => jsonl_encode([{"k": as_secret("hunter2", "PW")}])), "item 0[\"k\"] is secret(PW)"))
assert(contains(error_of(() => json_for_script(as_secret("hunter2", "PW"))), "a secret is never written as text"))
assert(not contains(error_of(() => json_encode(as_secret("hunter2", "PW"))), "hunter2"))
assert_eq(json_encode({"when": date(2026, 1, 31)}), "{\"when\": \"2026-01-31\"}")
assert_eq(json_encode([datetime("2026-01-03T10:00:00Z"), duration(hours = 1)]), "[\"2026-01-03T10:00:00Z\", \"PT1H\"]")
let back be json_decode(json_encode({"when": date(2026, 1, 31)}))
assert_eq(type_of(back.when), "text") -- JSON has no date type
assert_eq(date(back.when), date(2026, 1, 31)) -- re-type it on purpose
assert_eq(json_for_script({"s": "</script>"}), "{\"s\": \"\\u003c/script\\u003e\"}")
test "json_decode keeps integers of any size exact; numbers with . or an exponent are floats"
let big be json_decode("{\"id\": 12345678901234567890}")
assert_eq(is_integer(big.id), true)
assert_eq(text(big.id + 1), "12345678901234567891")
assert_eq(json_decode("[1.5, 2, 1e3]"), [1.5, 2, 1000.0])
assert_eq(is_integer(json_decode("1e3")), false)
assert_error(() => json_decode("1e400")) -- out of float range: an error, never inf
assert_eq(json_decode("{\"a\": 1, \"a\": 2}"), {"a": 2}) -- a repeated key: the last wins
assert_eq(json_decode("\u{feff}{\"a\": 1}"), {"a": 1}) -- a leading BOM is skipped
test "NaN and Infinity: json_encode writes them, json_decode reads them only with allow_nan = true"
assert_eq(json_encode([nan, inf, -inf]), "[NaN, Infinity, -Infinity]")
assert_error(() => json_decode("[NaN]")) -- not JSON: a NaN passes every x <= 0 check
let v be json_decode("[NaN, Infinity, -Infinity]", allow_nan = true)
assert(is_nan(v[0]))
assert_eq(v[1], inf)
assert_eq(v[2], -inf)
test "fallback form: json_decode(text, default) returns default instead of raising"
assert_eq(json_decode("{bad", nothing), nothing)
assert_eq(json_decode("{bad", {"d": 1}), {"d": 1})
assert_error(() => json_decode("{bad"))
test "JSON Lines: one value per line, every line ends with a newline"
let text_out be jsonl_encode([{"a": 1}, [2], 3])
assert_eq(text_out, "{\"a\": 1}\n[2]\n3\n")
assert_eq(jsonl_decode("{\"a\": 1}\n\n[2]\n"), [{"a": 1}, [2]]) -- blank lines skipped
let msg be ""
try
jsonl_decode("{\"a\": 1}\n{bad\n")
recover e
set msg to e
assert(contains(msg, "line 2")) -- the error names the line
assert_eq(jsonl_decode("{\n", nothing), nothing)
assert(is_nan(jsonl_decode("[NaN]\n", allow_nan = true)[0][0]))
test "a task or a generator is not data: json_encode refuses it and says where"
let msg be ""
try
json_encode({"f": (x) => x})
recover e
set msg to e
assert(contains(msg, "value[\"f\"] is a task"))
assert_error(() => json_encode([rng(1)]))
assert_error(() => jsonl_encode([rng(1)]))
assert_error(() => json_for_script({"g": rng(1)}))
test "limits: nesting deeper than 128 levels is an error; a big integer round-trips"
assert_error(() => json_decode(fmt("{a}1{b}", {"a": join(apply(range(0, 200), (i) => "["), ""), "b": join(apply(range(0, 200), (i) => "]"), "")})))
let n be 123456789012345678901234567890
assert_eq(json_decode(json_encode(n)), n)Encode & decode§
json_encode(value) -- value → JSON text
json_for_script(value) -- same JSON, `<` `>` `&` escaped \u00XX — SAFE inside an inline <script>
json_decode(text) -- JSON text → value (object→map, array→list, …)
json_decode(text, nothing) -- fallback form: the default instead of an error
json_decode(text, allow_nan = true) -- also reads NaN / Infinity / -Infinity
What encode does with special values§
secret→ an error (engine v0.6.46+), injson_encode,jsonl_encode,json_for_scriptandcsv_encode, also deep inside a map or a list. The error names the secret and where it is —json_encode: the value["auth"]["key"] is secret(K), and a secret is never written as text — pass the value to the request itself (in a header; in the body or the URL once it is bound:require secret("K") for net("host")) and the engine writes it at the socket, or leave it out of what you encode(jsonl_encodesaysitem 0["auth"]["key"]) — and never contains the value. Before v0.6.46 the secret came out as"[redacted]": the key never leaked, but the program got a marker it could send on as if it were the data. To migrate: pass the map with the secret straight to the request (http_post(url, {"key": k})oncekis bound to that host, see Secrets) — the engine writes it at the socket — or leave the secret out of what you encode. Agivefrom aserveroute still answers"[redacted]"(and warns in the server log).bytes→ a base64 string.decimal(1.50d) → an exact JSON number.nothing→null.date/datetime/duration→ their ISO 8601 text ("2026-01-31","2026-01-03T10:00:00Z","PT1H"). JSON has no date type, sojson_decodegives the text back: re-type it withdate(x)/datetime(x).nan,inf,-inf→NaN,Infinity,-Infinity(Python's form — not standard JSON; see below).- A task, a lambda or a generator from
rngis not data:json_encode,jsonl_encodeandjson_for_scriptrefuse it with an error that says where it is (json_encode: the value["f"] is a task, not data). Store what it computes, or leave it out.
This is the idiomatic way to store structured data in a text/Redis value:
redis_set("cfg", json_encode({"theme": "dark", "n": 3}))
let cfg be json_decode(redis_get("cfg"))
What decode gives back§
- Integers of any size stay exact (
{"id": 12345678901234567890}— a uint256 too), up to 4300 digits. A number with.or an exponent is a float (1e3→1000.0); one outside the float range (1e400) is an error, never infinity. NaNandInfinityare not JSON and are an error by default, because a NaN amount passes everyamount <= 0check. To read whatjson_encodewrote on purpose:json_decode(text, allow_nan = true).- A UTF-8 byte order mark at the start is skipped. A repeated key: the last one wins. Nesting deeper than 128 levels is an error.
- Invalid JSON is an error with line and column.
json_decode(text, default)returnsdefaultinstead — the way to validate untrusted input withouttry.
The same parser reads a response body (json of r: engine v0.6.20+; nothing for any other content type — see HTTP client), a serve request's JSON body and jsonl_decode.
JSON Lines§
One JSON value per line — the format of logs, exports and LLM datasets:
jsonl_encode([{"a": 1}, [2], 3]) -- "{\"a\": 1}\n[2]\n3\n" — every line ends with \n
jsonl_decode("{\"a\": 1}\n\n[2]\n") -- [{a: 1}, [2]] — blank lines are skipped
jsonl_decode(text, nothing) -- fallback form
jsonl_decode(text, allow_nan = true) -- NaN / Infinity, as in json_decode
A bad line is an error that names the line number. For files: jsonl_decode(read_file("events.jsonl")) and write_file("out.jsonl", jsonl_encode(rows)) — see Files & I/O.
XML — xml_parse (engine v0.6.20+)§
xml_parse(text) → a map, following the xmltodict convention (deterministic, no surprises): an element is a map, attributes are keys prefixed with @, a text-only element is its text, repeated children become a list, namespace prefixes stay in the name (cfdi:Emisor). The root is {"<root tag>": {...}}.
let x be xml_parse("<order id=\"7\"><item sku=\"a\"/><item sku=\"b\"/><note>hi</note></order>")
x["order"]["@id"] -- "7"
x["order"]["item"][1]["@sku"] -- "b" (two <item> → a list)
x["order"]["note"] -- "hi"
Malformed XML is an error with line and column. DTDs and external entities are not processed, on purpose (no XXE). There is no XML encoder. Pure — works in every profile.
TOML — toml_parse / toml_encode (engine v0.6.20+)§
toml_parse(text) → tables as maps, arrays and [[tables]] as lists, integers/floats/bools as numbers/bools, and TOML dates and times as ISO 8601 text — re-type them on purpose with date(x) / datetime(x). Dotted keys, inline tables and multi-line strings all work. toml_encode(map) → TOML text for JSON-like values; nothing is refused (TOML has no null — drop the key or give it a value), and so are bytes and secret. Pure.
let cfg be toml_parse(read_file("syn.toml"))
cfg["server"]["port"] + 1 -- a number, not text
print(toml_encode({"name": "demo", "db": {"host": "x", "port": 5432}}))