XCX_SPEC // AUTHORITATIVE_SOURCE

XCX 3.0 JSON and HTTP

JSON

JSON objects in XCX 3.0 are mutable.

Creation

json: config <<< {"port": 8080, "debug": false} >>>;
json: user   <<< {"name": "", "age": 0} >>>;

Values in the literal can be placeholders ("", 0, false) to be filled later via .set().

Serialization from Collections

You can also create JSON objects and arrays directly from XCX collections using the .toJson() method. This is available for: - Maps: Returns a JSON object. - Tables: Returns a JSON array of objects.

See the Collections Documentation for more details on mapping and behavior.

Parsing

json: parsed = json.parse(raw_string);

[!CAUTION] Panic on Invalid JSON (R305): If parsing fails, the VM terminates immediately. Verify string content before parsing.

Mutability Pattern

Declare a schema with zero-values, then populate:

json: resp <<< {"token": "", "role": "", "uid": 0, "ok": false} >>>;
resp.set("token", crypto.token(32));
resp.set("role",  "admin");
resp.set("uid",   42);
resp.set("ok",    true);
yield net.respond(200, resp);

JSON Methods

Method Signature Returns Description
.exists(path) (s) → b b Checks if path exists and is non-null
.get(path/idx) (s/i) → json json Gets element at path or index
.bind(path, var) (s, ref) → b b Extracts value into a pre-declared XCX variable
.set(path, val) (s, T) → b b Sets value at path; creates key if missing
.push(val) (json) → b b Appends element to a JSON array node
.size() / .count() () → i i Number of keys (object) or elements (array)
.toStr() () → s s Serializes to JSON string
.inject(path, map, tbl) (s, map, table) → b b Bulk import of JSON array into XCX table
.first() () → json json Returns the first element of a JSON array; halt.error if empty

[!NOTE] .push() on JSON arrays: .push() works exclusively on JSON nodes that are arrays ([]). Calling .push() on a JSON object ({}) results in a halt.error.

json: data <<< {"items": []} >>>;
json: obj <<< {"id": 1} >>>;
data.get("items").push(obj);   --- OK: "items" is an array
data.push(obj);                --- halt.error: data is an object, not an array

[!IMPORTANT] .bind() syntax: The second argument must be a previously declared variable. You cannot declare the type inline.

--- Wrong
req.bind("ip", s: ip);

--- Correct
s: ip;
req.bind("ip", ip);

Path Notation

Both dot-notation and bracket notation are supported for nested access:

--- Nested field
json: cfg <<< {"server": {"host": "localhost"}} >>>;
s: host;
cfg.bind("server.host", host);

--- Array index
json: resp <<< {"items": []} >>>;
resp.set("items[0]", first_item);
resp.set("items[1]", second_item);

.inject() — Bulk Import

Import a JSON array directly into an XCX table:

json: data <<< {"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]} >>>;
table: imported { columns=[uid::i, uname::s] rows=[EMPTY] };
map: mapping { schema=[s<->s] data=["uid"::"id", "uname"::"name"] };
data.inject("users", mapping, imported);
imported.show();

JSON in HTTP Requests

Inside HTTP handlers, json: req has this structure:

{
    "method":  "POST",
    "path":    "/api/login",
    "query":   { "page": "1" },
    "headers": { "authorization": "Bearer ..." },
    "body":    { ... },
    "ip":      "1.2.3.4"
}
s: ip;
req.bind("ip", ip);

json: body;
req.bind("body", body);

json: headers;
req.bind("headers", headers);

s: auth;
headers.bind("authorization", auth);

HTTP Client

High-Level API

Method Signature Returns
net.get(url) (s) → json json
net.post(url, body) (s, json) → json json
net.put(url, body) (s, json) → json json
net.delete(url) (s) → json json
json: resp = net.get("https://api.example.com/users");

json: body <<< {"name": "Alice"} >>>;
json: resp = net.post("https://api.example.com/users", body);

Response Object

{
    "status":  200,
    "ok":      true,
    "body":    { ... },
    "headers": { "content-type": "application/json" },
    "text":    "...",
    "error":   "..."
}
Pole Typ Opis
status i HTTP status code
ok b true gdy status >= 200 i < 300
body json Sparsowane ciało odpowiedzi (jeśli JSON); inaczej null
headers json Nagłówki odpowiedzi
text s Ciało odpowiedzi jako surowy string (dostępne zawsze)
error s Komunikat błędu przy niepowodzeniu żądania; pusty string jeśli OK

ok is true when status >= 200 and < 300. Always check ok before accessing body.

json: resp = net.get("https://api.example.com/data");
if (resp.ok) then;
    --- praca z resp.body
else;
    s: err;
    if (resp.exists("error")) then;
        resp.bind("error", err);
    end;
    >! "Error: " + s(resp.get("status")) + " | " + err;
end;

Low-Level Builder (net.request)

net.request {
    method  = "POST",
    url     = "https://api.example.com/data",
    headers = ["Authorization" :: "Bearer xyz", "X-App" :: "XCX"],
    body    = my_json,
    timeout = 5000
} as resp;
Field Type Required Default Description
method s Yes "GET", "POST", "PUT", etc.
url s Yes Full URL with scheme
headers map:s<->s No {} Additional request headers
body json No null Ignored for GET and DELETE
timeout i No 10000 Milliseconds

HTTP Server

serve Directive

serve: app {
    port    = 8080,
    host    = "0.0.0.0",
    workers = 4,
    routes  = [
        "POST   /api/login"     :: handle_login,
        "GET    /api/user"      :: handle_user,
        "DELETE /api/users"     :: handle_delete,
        "OPTIONS *"             :: handle_options,
        "*"                     :: handle_404
    ]
};
Field Type Required Default Description
port i Yes Port number (1–65535)
host s No "127.0.0.1" "0.0.0.0" = all interfaces
workers i No 1 Fibers per request
routes route list Yes Checked top-to-bottom, first-match wins

[!NOTE] serve: is a terminal statement. No code after this directive will execute. Wildcard * matches any method or path — always place it last.

Handler Fibers

Every handler must be a fiber with the signature fiber name(json: req -> json):

fiber handle_health(json: req -> json) {
    yield net.respond(200, <<< {"status": "ok"} >>>);
};

A handler that does not call yield net.respond(...) results in an automatic 500 Internal Server Error.

net.respond()

yield net.respond(200, my_json);
yield net.respond(201, my_json, ["Location" :: "/users/42"]);
yield net.respond(204, <<< {} >>>);
yield net.respond(404, <<< {"error": "not found"} >>>);
Parameter Type Required Description
status i Yes HTTP status code
body json | s Yes JSON object or raw string; use <<< {} >>> for empty JSON
headers map:s<->s No Additional response headers

CORS and Preflight

By default, the XCX engine provides automatic CORS support by adding these headers if they are missing from the response: - Access-Control-Allow-Origin: * - Access-Control-Allow-Methods: GET, POST, OPTIONS, DELETE, PATCH - Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-TOKEN

[!TIP] Recommended Practice: While the engine provides defaults, it is highly recommended to explicitly declare these headers in your code. This ensures your application remains predictable across different environments and clearly documents its security policy.

For preflight (OPTIONS) support, use a dedicated handler to explicitly set allowed methods and headers:

fiber handle_options(json: req -> json) {
    yield net.respond(204, <<< {} >>>, [
        {
            "Access-Control-Allow-Methods": "GET, POST, DELETE, PATCH, OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type, Authorization, X-CSRF-TOKEN"
        }
    ]);
};

Concurrent Requests

Fibers allow overlapping I/O:

fiber fetch(s: url -> json) {
    yield net.get(url);
};

fiber:json: f1 = fetch("https://api.example.com/users");
fiber:json: f2 = fetch("https://api.example.com/posts");
json: r1 = f1.next();
json: r2 = f2.next();

Security Constraints

Constraint Behavior
localhost / 127.0.0.1 Allowed by default
169.254.x.x (link-local) halt.fatal — SSRF protection
10.x, 172.16.x, 192.168.x Blocked in production mode
file:// URLs halt.fatal
Max response body size 10 MB
Max incoming request body 10 MB — returns 413 without invoking the handler
UP ↑