JSON

Loft has built-in JSON support, and there are two ways in. Reach for the one that matches what you know about the document:

* you know its SHAPE — it maps onto a struct you have declared. Serialise
  with the ':j' format flag and parse with 'Type.parse(text)'. No
  annotations and no code generation: it works from the declaration.
* you do NOT know its shape, or you have to report precisely what was wrong
  with it. Parse to a 'JsonValue' with 'json_parse(text)' and walk it.

Both are built in and neither is going away. The struct form is shorter and is what most programs want; the JsonValue form is the one that can answer "what is in here?" and "what exactly was wrong?".

Serialisation — struct to JSON

Use the :j format flag inside a format string to produce JSON output. Field names become quoted keys; strings are escaped (quotes, backslashes, control characters); numbers and booleans are written as JSON literals.

"{my_struct:j}"
struct User {
  id: integer,
  name: text,
  email: text
}
struct MaybeUser {
  id: integer,
  name: text?,
}

Parsing — JSON to struct

Call Type.parse(text) to create a struct from JSON text. A text argument is wrapped through json_parse internally, so the one-step form is the whole story for a document you expect to be well-formed.

A field the JSON does not mention — and a field written null — gets the DECLARED type's absent value. For a plain field that is its zero, because a plain field cannot hold null; write the field integer? / float? if you need to tell "absent" from "zero" apart:

struct Reading { id: integer, drift: float? }
r = Reading.parse(`{{}}`)     // r.id == 0, r.drift == null

Note the backticks and the doubled braces: every loft string is a format string, so a JSON literal written in source needs {{ and }} for its own braces (see the Safety page).

Vectors

Parse a JSON array into a vector of structs with vector<T>.parse(text).

scores = vector<Score>.parse(`[{{"value":1}},{{"value":2}}]`)
struct Score {
  value: integer
}
struct Item { key: text, count: integer }
struct Catalogue { all: vector<Item>, by_key: hash<Item[key]> }

What went wrong — json_errors() and #errors

Both parse forms report. json_errors() holds the LAST parse's diagnostics and is replaced by the next parse, so a successful parse leaves it empty. It is a TEXT, not a collection — several diagnostics are joined with | — so ask json_errors() != "", not len(...) > 0, and never loop over it.

if json_errors() != "" { log_warn(json_errors()); }

record#errors answers for the Type.parse(…) that built that record, as a vector<text> with one entry per value that did not fit — every mismatch, whether the one-step Type.parse(text) or the staged Type.parse(json_parse(text)) built it; a field whose value did not fit holds the type's default, the rest is read. Each parsed record keeps its own:

for e in user#errors { log_warn(e); }

The two parse spellings differ in WHICH HALF of the answer they give, and that is the reason to choose between them:

User.parse(text)               ->  line 1:33 path:addr.zip
User.parse(json_parse(text))   ->  Addr.zip: expected JNumber, got JString

The one-step form says WHERE in the document, by line, column and field path. The staged form says WHAT was wrong, by declared type against found type — and on malformed input it renders the offending line with a caret. Take the first when a human will look at the document, the second when your program has to explain the mismatch.

Nested Structs

Structs with struct-typed fields parse nested JSON objects automatically.

struct Address {
  city: text,
  zip: text
}
struct Contact {
  name: text,
  address: Address
}

Reading a document whose shape you do not know

json_parse(text) answers a JsonValue — a tree you can ask about instead of a struct you had to declare first. kind() names the variant, field(name) and item(i) step into objects and arrays, keys() lists an object's field names, and the as_* extractors answer null on a kind mismatch rather than faulting. Every miss answers JNull, so a walk over an unexpected document degrades instead of stopping.

struct Tagged { tag: text, n: integer }

Used by the wire-shape section at the end.

enum Shade { Red, Green }
struct Flag   { on: Shade }
struct Pair   { p: (integer, text) }
struct Holder { n: integer, rows: vector<Item>? }
fn main() {
  u = User { id: 42, name: "Alice", email: "alice@example.com" };
  json = "{u:j}";
  expected = `{{"id":42,"name":"Alice","email":"alice@example.com"}}`;
  assert(json == expected, "to json");
  bob = User.parse(`{{"id":7,"name":"Bob","email":"bob@test.org"}}`);
  assert(bob.id == 7, "parsed id: {bob.id}");
  assert(bob.name == "Bob", "parsed name");
  assert(bob.email == "bob@test.org", "parsed email");
  u2 = User.parse("{u:j}");
  assert(u2.id == u.id, "round-trip id");
  assert(u2.name == u.name, "round-trip name");

Escaping is not optional and not yours to do: a quote, a backslash and a control character all come back as JSON requires.

  quoted = User { id: 1, name: "a\"b\\c", email: "x\ny" };
  assert("{quoted:j}" == `{{"id":1,"name":"a\\"b\\\\c","email":"x\\ny"}}`,
         "text is escaped: {quoted:j}");

A type-mismatched field leaves the record at its DECLARED default — id is plain, so that is 0, never a null in a slot the declaration says cannot hold one — and the mismatch is reported through json_errors().

  bad = User.parse(`{{"id":"not_a_number","name":42}}`);
  assert(bad.id == 0, "type-mismatched id keeps its default: {bad.id}");
  assert(json_errors() != "", "the mismatch was reported: [{json_errors()}]");

…and EVERY mismatch is named, on the record: id and name both, in either form.

  assert(len(bad#errors) == 2, "both mismatches on the record: {bad#errors}");
  staged = User.parse(json_parse(`{{"id":"not_a_number","name":42}}`));
  assert(len(staged#errors) == 2, "the staged form names both too: {staged#errors}");

…and the next parse replaces it, so a good parse reads clean.

  fine = User.parse(`{{"id":2,"name":"n","email":"e"}}`);
  assert(fine.id == 2, "the good parse landed");
  assert(json_errors() == "", "a successful parse clears the errors: [{json_errors()}]");
  scores = vector<Score>.parse(`[{{"value":10}},{{"value":20}},{{"value":30}}]`);
  total = 0;
  for s in scores {
    total += s.value;
  }
  assert(total == 60, "vector sum: {total}");

An element the declaration cannot take keeps its slot at the defaults, and the elements around it are read — as a struct keeps its other fields; #errors names it.

  spoiled = vector<Score>.parse(`[{{"value":"x"}},{{"value":2}}]`);
  assert(len(spoiled) == 2, "one bad element keeps its slot: {len(spoiled)}");
  assert(spoiled[0].value == 0 && spoiled[1].value == 2, "the fitting element is read: {spoiled}");
  assert(len(spoiled#errors) == 1, "the mismatch is named: {spoiled#errors}");
  c = Contact.parse(`{{"name":"Carol","address":{{"city":"Amsterdam","zip":"1012"}}}}`);
  assert(c.name == "Carol", "nested name");
  assert(c.address.city == "Amsterdam", "nested city: {c.address.city}");
  assert(c.address.zip == "1012", "nested zip");

A missing field gets its own declared absence

When a field is absent from the JSON, it gets the absent value its DECLARATION allows — not the type's null sentinel. A plain field cannot hold null, so it takes its empty value: 0 for a number, "" for text, false for a boolean. Declare the field T? when "the document did not say" has to be tellable from "the document said zero", and check it with ! or ?? before use.

  partial = User.parse(`{{"id":1}}`);
  assert(partial.id == 1, "partial id");
  assert(partial.name == "", "a missing `text` field is empty: [{partial.name}]");
  maybe = MaybeUser.parse(`{{"id":1}}`);
  assert(!maybe.name, "a missing `text?` field is null");

Walking a document by shape rather than by declaration.

  doc = json_parse(`{{"tag":"reading","n":3,"items":["a","b"]}}`);
  assert(doc.kind() == "JObject", "the document is an object: {doc.kind()}");
  assert(doc.has_field("tag"), "it carries `tag`");
  assert((doc.field("n").as_long() ?? -1) == 3, "n is 3");
  assert((doc.field("tag").as_text() ?? "") == "reading", "tag is `reading`");
  assert(len(doc.keys()) == 3, "three keys: {len(doc.keys())}");
  items = doc.field("items");
  assert(items.kind() == "JArray", "items is an array");
  assert((items.item(1).as_text() ?? "") == "b", "second item is `b`");

A field that is not there is JNull, not a fault — so a walk keeps going.

  assert(doc.field("absent").kind() == "JNull", "a missing field answers JNull");
  assert(!doc.field("n").as_text(), "an `as_` on the wrong kind answers null");

Once you know the shape, hand the same tree to the struct form.

  t = Tagged.parse(doc);
  assert(t.tag == "reading", "the tree parses into a struct too: {t.tag}");

Building a document without a struct to describe it.

  reply = json_object([
    JsonField { name: "ok", value: json_bool(true) },
    JsonField { name: "n",  value: json_number(42.0) },
  ]);
  assert(reply.to_json() == `{{"ok":true,"n":42}}`, "built: {reply.to_json()}");

The shapes on the wire

Four encodings a reader interoperating with another system needs, none of which the field declaration makes obvious: * a plain enum variant is an OBJECT keyed by the variant name — Green is {{"Green":{{}}}}, not the string "Green"; * a tuple is an object with _0, _1 … keys, not a JSON array; * an absent field is written null, an absent vector<T>? too — it is not [], which is the present-but-empty collection and a different value; * a keyed member of a linked collection group is left out as well, because it is an INDEX on the same rows rather than data of its own — and parsing the document back rebuilds it from them.

  colour = Flag { on: Shade.Green };
  assert("{colour:j}" == `{{"on":{{"Green":{{}}}}}}`, "an enum variant: {colour:j}");
  pair = Pair { p: (7, "seven") };
  assert("{pair:j}" == `{{"p":{{"_0":7,"_1":"seven"}}}}`, "a tuple: {pair:j}");
  nothing = Holder { n: 1, rows: null };
  assert("{nothing:j}" == `{{"n":1,"rows":null}}`, "an absent collection is written null: {nothing:j}");
  something = Holder { n: 1, rows: [] };
  assert("{something:j}" == `{{"n":1,"rows":[]}}`, "an empty one is not: {something:j}");
  assert(Holder.parse("{nothing:j}").rows == null, "absent comes back absent");
  assert(!(Holder.parse("{something:j}").rows == null), "empty comes back empty");

The index is derived, so it costs nothing on the wire and is rebuilt on the way back in.

  cat2 = Catalogue { all: [Item{key:"a", count:1}], by_key: [] };
  assert("{cat2:j}" == `{{"all":[{{"key":"a","count":1}}]}}`,
         "the index is not written: {cat2:j}");
  back = Catalogue.parse("{cat2:j}");
  assert(len(back.by_key) == 1, "…and is rebuilt on the way in: {len(back.by_key)}");
  assert(back.by_key["a"].count == 1, "the rebuilt index answers a lookup");
}