Synsemadocsv0.6.xENES

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.

json.syn
-- 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§

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§

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}}))