docs

Working with JSON

Parse JSON documents, inspect nested values, and serialize data back to text.

On this page

Ultralight uses the JSON type to represent parsed JSON documents. It lets you inspect nested values, build or modify data, and serialize documents back to text— operations never throw exceptions.

Creating Documents

Parsing Text

Pass a string to JSON::Parse() to parse a document:

C++
JSON config = JSON::Parse(text);
if (!config)
  printf("line %u: %s\n", config.error_line(),
         config.error_message().utf8().data());

Testing a JSON object in a conditional evaluates to true when parsing succeeds and false when it fails.

When parsing fails, error_line() returns the line number of the syntax error, and error_message() describes the problem.

Empty Documents

Construct a new empty document using JSON::Object() or JSON::Array():

C++
JSON save = JSON::Object();
JSON scores = JSON::Array();

Both methods return empty root documents ready for nested values.

Reading Values

Chained Lookups

Chain operator[] across keys or indices and call Or() to read a value with a fallback:

C++
void ApplyConfig(const JSON& config) {
  double scale = config["ui"]["scale"].Or(1.0);  // 1.0 if missing
  String name = config["profile"]["name"].Or("anonymous");
  bool muted = config["audio"]["muted"].Or(false);
}

You can chain lookups across nested objects and arrays without checking each step. If a key is missing or an index is out of bounds, the chain safely resolves to the fallback passed to Or().

The Or() method does not convert types— reading a string "3" with Or(0) returns the fallback 0.

Always read values through a const reference. Chaining operator[] on a non-const document creates missing keys automatically.

🚧 Testing Boolean Keys

Testing a lookup like if (config["enabled"]) checks whether the key exists, not whether its value is true. A key set to false still evaluates to true in a conditional. Use Or(false) to read a JSON boolean value.

Iterating Collections

Step through object members or array elements using range-for loops with AsObject() or AsArray():

C++
for (auto [key, value] : config["servers"].AsObject())
  Connect(key, value.Or(""));

Object iteration visits keys in the order they appear in the source text.

Calling AsObject() or AsArray() on the wrong type or a missing key produces an empty loop rather than an error.

Writing Values

Setting Values

Assign values to nested keys using operator[] on a non-const document:

C++
save["player"]["name"] = "ada";  // creates "player" first
save["player"]["level"] = 12;

Assigning through nested keys on a non-const document creates missing objects along the path automatically.

Modifying Arrays

Update array elements by index or append new items with Push():

C++
save["flags"] = JSON::Array();
save["flags"][0] = "tutorial_done";

JSON flags = save["flags"];
flags.AsArray().Push("first_boss");

Assigning to an index updates that element directly, while AsArray() provides access to array methods like Push().

Serializing to Text

Convert a document back to text using Stringify():

C++
String compact = save.Stringify();
String pretty = save.Stringify(2);  // two spaces per level

Calling Stringify() without arguments formats the document as a compact, single-line string.

Passing an indentation size formats the text across multiple lines with that number of spaces per level.

Copying Documents

Duplicate a document with Clone() or assign it to share the underlying data:

C++
JSON shared = save;        // the same document
JSON copy = save.Clone();  // an independent copy

Copying a JSON instance shares the underlying document— a modification made through one handle is visible across every copy.

Call Clone() when you need an independent, deep copy.