Biblioteca estándar
JSON, XML y TOML
Dos builtins, round-trippables: para valores con forma de JSON (maps, listas, texto, números, bools, nothing) json_decode(json_encode(x)) reconstruye x — enteros grandes incluidos. Más JSON Lines, xml_parse y 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)Codificar y decodificar§
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
Qué hace encode con valores especiales§
secret→ un error (motor v0.6.46+), enjson_encode,jsonl_encode,json_for_scriptycsv_encode, también adentro de un map o una lista. El error nombra el secret y dónde está —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_encodediceitem 0["auth"]["key"]) — y nunca contiene el valor. Antes de v0.6.46 el secret salía como"[redacted]": la clave no se filtraba, pero el programa recibía una marca que podía mandar como si fuera el dato. Para migrar: pasá el map con el secret directo a la request (http_post(url, {"key": k})una vez quekestá ligado a ese host, ver Secrets) — el motor lo escribe en el socket — o dejá el secret afuera de lo que codificás. Ungivede una ruta deservesigue respondiendo"[redacted]"(y avisa en el log del servidor).bytes→ un string base64.decimal(1.50d) → un número JSON exacto.nothing→null.date/datetime/duration→ su texto ISO 8601 ("2026-01-31","2026-01-03T10:00:00Z","PT1H"). JSON no tiene tipo fecha, así quejson_decodedevuelve el texto: re-tipalo condate(x)/datetime(x).nan,inf,-inf→NaN,Infinity,-Infinity(la forma de Python — no es JSON estándar; ver abajo).- Una task, una lambda o un generador de
rngno son datos:json_encode,jsonl_encodeyjson_for_scriptlos rechazan con un error que dice dónde están (json_encode: the value["f"] is a task, not data). Guardá lo que calcula, o dejalo afuera.
Es la forma idiomática de guardar datos estructurados en un valor de texto/Redis:
redis_set("cfg", json_encode({"theme": "dark", "n": 3}))
let cfg be json_decode(redis_get("cfg"))
Qué devuelve decode§
- Los enteros de cualquier tamaño quedan exactos (
{"id": 12345678901234567890}— un uint256 también), hasta 4300 dígitos. Un número con.o exponente es un float (1e3→1000.0); uno fuera del rango de float (1e400) es un error, nunca infinito. NaNeInfinityno son JSON y por defecto son un error, porque un monto NaN pasa todo chequeoamount <= 0. Para leer lo quejson_encodeescribió a propósito:json_decode(text, allow_nan = true).- Una marca de orden de bytes UTF-8 al principio se saltea. Una clave repetida: gana la última. Anidar más de 128 niveles es un error.
- El JSON inválido es un error con línea y columna.
json_decode(text, default)devuelvedefaulten su lugar — la forma de validar entrada no confiable sintry.
El mismo parser lee el cuerpo de una respuesta (json of r: engine v0.6.20+; nothing para cualquier otro content type — ver Cliente HTTP), el cuerpo JSON de un request de serve y jsonl_decode.
JSON Lines§
Un valor JSON por línea — el formato de logs, exports y datasets para LLMs:
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
Una línea mala es un error que nombra el número de línea. Para archivos: jsonl_decode(read_file("events.jsonl")) y write_file("out.jsonl", jsonl_encode(rows)) — ver Archivos e I/O.
XML — xml_parse (engine v0.6.20+)§
xml_parse(text) → un map, con la convención de xmltodict (determinista, sin sorpresas): un elemento es un map, los atributos son claves con prefijo @, un elemento que solo tiene texto es su texto, los hijos repetidos pasan a ser una lista, los prefijos de namespace se conservan en el nombre (cfdi:Emisor). La raíz es {"<tag raíz>": {...}}.
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"
Un XML malformado es un error con línea y columna. Las DTD y las entidades externas no se procesan, a propósito (sin XXE). No hay codificador de XML. Puro — funciona en todos los perfiles.
TOML — toml_parse / toml_encode (engine v0.6.20+)§
toml_parse(text) → las tablas como maps, los arrays y las [[tablas]] como listas, enteros/flotantes/bools como números/bools, y las fechas y horas TOML como texto ISO 8601 — re-tipalas a propósito con date(x) / datetime(x). Claves con punto, tablas inline y strings multilínea funcionan. toml_encode(map) → texto TOML para valores tipo JSON; nothing se rechaza (TOML has no null — drop the key or give it a value), igual que bytes y secret. Puro.
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}}))