Skip to content

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.

01-encode.zolo
Playground
// 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.

02-decode.zolo
Playground
// 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.

03-nested.zolo
Playground
// 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.

04-error-handling.zolo
Playground
// 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.

05-tagged-template.zolo
Playground
// 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.

06-path-walk.zolo
Playground
// 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.

07-path-missing.zolo
Playground
// 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.

08-schema-interop.zolo
Playground
// 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
enespt-br