JSON (std::json)
std::json converts between Zolo values and JSON text. #{} maps become JSON
objects, arrays become JSON arrays, and primitives map directly.
Encode (serialisation)
json.encode(value) — alias json.stringify — produces a JSON string:
Primitives, maps and arrays; stringify is the same as encode.
// Feature: json.encode — serialize a Zolo value to a JSON string
// When to use: send data over HTTP, save to file, structured logs.
use std::json
// Primitive types map 1:1 to JSON.
print(json.encode(42)) // expected: 42
print(json.encode(3.14)) // expected: 3.14
print(json.encode(true)) // expected: true
print(json.encode(nil)) // expected: null
print(json.encode("hello")) // expected: "hello"
// Map literal becomes a JSON object.
let user = #{name: "Alice", age: 30, active: true}
print(json.encode(user)) // expected: {"name":"Alice", ...}
// Array becomes a JSON array.
const nums = [1, 2, 3, 4, 5]
print(json.encode(nums)) // expected: [1,2,3,4,5]
// `stringify` is an alias for `encode` (JS-style).
print(json.stringify(#{k: "v"})) // expected: {"k":"v"}
Decode (deserialisation)
json.decode(string) — alias json.parse — converts JSON into a Zolo value.
Use raw strings r#"..."# to avoid the ceremony of escaping quotes:
Object and array decoded; parse is the same as decode.
// Feature: json.decode — convert a JSON string to a Zolo value
// When to use: parse API payloads, read JSON config files.
// For inline JSON literals use the tagged literal `json"..."` — it is more
// readable than escaping everything with `\"` and `\{`.
use std::json
// Decode primitives. `json.decode` takes a STRING; the tagged
// literal `json"..."` is itself a parsed Zolo value (table), so use
// raw strings (`r#"..."#`) as input here to avoid escape ceremony.
print(json.decode("42")) // expected: 42
print(json.decode(r#""hi""#)) // expected: hi
print(json.decode("true")) // expected: true
print(json.decode("null")) // expected: nil
// Decode object -> map.
let obj = json.decode(r#"{"name":"Bob","age":25}"#)
print(obj["name"]) // expected: Bob
print(obj["age"]) // expected: 25
// Decode array. Arrays are 0-indexed via `[]` in Zolo.
let arr = json.decode("[10, 20, 30]")
print(arr[0]) // expected: 10
print(arr[2]) // expected: 30
// `parse` is an alias for `decode` (JS-style).
let parsed = json.parse(r#"{"ok":true}"#)
print(parsed["ok"]) // expected: true
// Tip: when you have inline JSON, the tagged literal `json"..."`
// returns a Zolo value directly — no decode call needed.
Nested Structures
Objects inside arrays and vice versa — round-trip without loss:
Encode → decode preserves the hierarchy of maps and arrays.
// Feature: json — nested values (objects inside arrays and vice versa)
// When to use: complex JSON documents from REST APIs.
use std::json
// Object that contains an array.
let post = #{
id: 1,
title: "Hello",
tags: ["zolo", "json", "stdlib"],
author: #{name: "Alice", email: "[email protected]"},
}
let encoded = json.encode(post)
print(encoded)
// Round-trip: encode -> decode -> same structure.
let decoded = json.decode(encoded)
print(decoded["title"]) // expected: Hello
print(decoded["tags"][1]) // expected: json
print(decoded["author"]["name"]) // expected: Alice
// Array of objects.
let users = [
#{name: "Alice", age: 30},
#{name: "Bob", age: 25},
]
let raw = json.encode(users)
let back = json.decode(raw)
print(back[0]["name"]) // expected: Alice
print(back[1]["age"]) // expected: 25
Error Handling
json.decode returns (nil, error_msg) when the input is invalid. The
idiomatic pattern is to wrap it in a function that returns a Result:
Double destructuring let v, e = json.decode(s) to check for failures.
// Feature: json.decode — error handling
// When to use: parsing payloads with no guarantee they are valid JSON.
// json.decode returns (nil, err_msg) on failure — standard return protocol.
use std::Result
use std::json
// json.decode takes a STRING — use raw strings to avoid escapes.
let valid = json.decode(r#"{"ok":true}"#)
print(valid["ok"]) // expected: true
// Invalid input -> value is nil, err carries a message.
let bad, err = json.decode("garbage payload")
print(bad) // expected: nil
print(err != nil) // expected: true
// Idiomatic pattern: wrap in a Result.
fn safe_parse(s: str) {
let v, e = json.decode(s)
if e != nil { return Result.Err(e) }
return Result.Ok(v)
}
let r1 = safe_parse("42")
let r2 = safe_parse("not valid json text")
print(r1.is_ok()) // expected: true
print(r2.is_err()) // expected: true
The json"..." Literal
The tagged literal json#"..."# is resolved at compile time — no call to
decode at runtime, no escapes:
Configuration defaults as a compiled JSON literal.
// Feature: `json"..."` — JSON literal as a tagged template
// Syntax: the template body is parsed as JSON at compile time and
// emitted as a Map. No runtime decode call, no escaping ceremony.
// When to use: defaults / fixtures / inline configs that are easier
// to read as JSON than as `#{...}` map literals.
use std::Map
let defaults = json#"{ "port": 8080, "host": "localhost", "debug": true }"#
print(defaults["port"])
// expected: 8080
print(defaults["host"])
// expected: localhost
print(defaults["debug"])
// expected: true
// For simple inline maps, the `#{...}` map literal is more idiomatic
// than the `json"..."` tagged template — no JSON parsing, just keys.
let cfg = #{
name: "alice",
age: 30,
active: true,
}
print(cfg["name"]) // expected: alice
print(cfg["age"]) // expected: 30
print(cfg["active"]) // expected: true
Path Walk
value.path("a.b.0.c") navigates a nested map with dot-separated segments;
numeric segments index arrays:
path and has_path for accessing and checking deep keys.
// Feature: `data.path("a.b.0.c")` — walk a nested map by dotted segments
// Syntax: numeric segments are 0-based array indices; everything else
// is a map key. `data.has_path("a.b")` is the boolean wrapper.
// When to use: pluck a single value out of a deeply-nested config or
// JSON response without writing a chain of `["a"]["b"]` indexes.
let data = #{
"users": [
#{ "name": "alice", "role": "admin" },
#{ "name": "bob", "role": "dev" },
],
"config": #{
"port": 8080,
"debug": true,
},
}
print(data.path("config.port")) // expected: 8080
print(data.path("config.debug")) // expected: true
print(data.path("users.0.name")) // expected: alice
print(data.path("users.1.role")) // expected: dev
// `has_path` returns whether the value exists.
print(data.has_path("config.port")) // expected: true
print(data.has_path("config.missing")) // expected: false
Missing Values via Path
path returns nil for non-existent segments; has_path distinguishes
"absent" from "present but false":
has_path("config.debug") is true even when the value is false.
// Feature: `path` and `has_path` semantics for missing / falsy values
// Rules:
// - missing segment → `nil`
// - walking through a non-table → `nil`
// - `has_path` returns `false` for missing, `true` for present-but-falsy
// When to use: distinguish "not provided" from "set to false" in
// settings / feature flags / JSON inputs.
let data = #{
"config": #{
"port": 8080,
"debug": false,
},
}
// Missing segment → nil.
print(data.path("config.missing"))
// expected: nil
print(data.path("nope.also.missing"))
// expected: nil
// `has_path` is the boolean wrapper.
print(data.has_path("config.missing"))
// expected: false
// A `false` *value* is still present.
print(data.has_path("config.debug"))
// expected: true
// Walking past a leaf (port is an int) gives nil.
print(data.path("config.port.deeper"))
// expected: nil
Schema Interop
Every schema declaration automatically gains from_json(str) -> Result and
to_json() -> str:
Point.from_json validates + decodes; p.to_json() serialises the instance.
// Feature: `Schema.from_json` / `instance.to_json()` round-trip
// Syntax: every `schema` declaration auto-generates `from_json(str)`
// (decodes + parses, returns Result) and `instance.to_json()` (encodes
// via to_map + json.encode).
// When to use: end-to-end validation of JSON requests/responses.
// Combines the validation in `features/23-schemas/` with
// `json.encode` / `json.decode`.
use std::Result
use std::json
schema Point {
x: int,
y: int,
}
// from_json: decode JSON, then run through Schema.parse so all
// `where` constraints fire. Returns Result.
let raw = "\{ \"x\": 3, \"y\": 4 \}"
match Point.from_json(raw) {
Result::Ok(p) => {
print(p.x) // expected: 3
print(p.y) // expected: 4
},
Result::Err(e) => print("error: {e}"),
}
// to_json: instance → JSON string. Key order is not stable, but the
// substrings are. (We use `Point.new(...)` here — the factory built
// for every schema.)
let p = Point.new(3, 4)
let s = p.to_json()
print(s.contains("\"x\":3")) // expected: true
print(s.contains("\"y\":4")) // expected: true