Skip to content

RaftJsonAdvanced

Rob Dobson edited this page May 3, 2026 · 1 revision

Advanced RaftJson

This page covers the parts of the RaftJson API that go beyond simple getString / getLong / getBool calls: chaining, prefix scoping, persistence to non-volatile storage, change callbacks, JSON generation and string escaping.

If you have not read it yet, start with JSON / RaftJson for the basics, and Configuration for how RaftJson is applied to SysMod configuration layering.


The class hierarchy

Class Header Purpose
RaftJsonIF RaftJsonIF.h Pure-virtual interface that every JSON source implements. APIs that take "any JSON document" should accept const RaftJsonIF&.
RaftJson RaftJson.h The standard implementation. On-demand parser over an in-memory string.
RaftJsonNVS RaftJsonNVS.h A RaftJson that loads/persists its document from a named ESP-IDF NVS namespace. Supports change callbacks.
RaftJsonPrefixed RaftJsonPrefixed.h A wrapper around an existing RaftJsonIF that prepends a fixed path prefix to every lookup.

All four expose the same accessor surface (getString, getLong, getDouble, getBool, getArrayElems, getKeys, contains, getType, …) so you can usually pass any of them anywhere a const RaftJsonIF& is expected.


Chaining (cascading lookups)

Every RaftJson and RaftJsonNVS instance can have a chained fallback object. When a path is not present in the primary document, the lookup is automatically retried in the chained document. This is how Raft's three-layer configuration model is implemented.

Constructing with a chain:

RaftJson defaultDoc(R"({"led":{"pin":2,"brightness":50}})", false);
RaftJson sysTypeDoc(R"({"led":{"pin":5}})", true, nullptr, &defaultDoc);

int pin        = sysTypeDoc.getInt("led/pin", -1);        // → 5  (from primary)
int brightness = sysTypeDoc.getInt("led/brightness", 0);  // → 50 (from chained default)

Setting / clearing the chain after construction:

sysTypeDoc.setChainedRaftJson(&defaultDoc);  // attach
sysTypeDoc.setChainedRaftJson(nullptr);      // detach

Notes:

  • The chain is followed per path lookup — there is no merge step and no copy. Walking the chain happens lazily each time a value is fetched.
  • Chains can be longer than two — each link is itself a RaftJsonIF, which may have its own chained document. Walking stops at the first match.
  • getKeys on an object returns keys from whichever document the object was first found in; it does not union keys across the chain.
  • Chained lookup uses the full path each time, so getString("a/b/c") on the primary, when a/b exists in the primary but c only in the fallback, will not fall through to the fallback (the path is considered "found, but no c"). Keep this in mind when splitting config across layers — split at object boundaries.

This mirrors the way W3C cascading style sheets work and is the mechanism behind compiled-default → SysType → NV-mutable layering described in Configuration.


Prefix scoping (RaftJsonPrefixed)

RaftJsonPrefixed wraps an existing RaftJsonIF and silently prepends a path prefix to every accessor. This is how each SysMod sees only its own slice of the system config.

RaftJson root(R"({"NetworkManager":{"WiFiSSID":"home","RetryMs":5000}})");
RaftJsonPrefixed netCfg(root, "NetworkManager");

String ssid = netCfg.getString("WiFiSSID", "");   // looks up "NetworkManager/WiFiSSID"
long retry  = netCfg.getLong("RetryMs", 1000);    // looks up "NetworkManager/RetryMs"

Properties:

  • The wrapper holds a reference to the underlying RaftJsonIF. The underlying object must outlive the wrapper.
  • Wrapping is zero-copy and zero-parse.
  • A prefixed view inherits the underlying object's chain. Lookups still fall through to chained documents.
  • The internal prefix is the supplied string with / appended automatically; pass "NetworkManager", not "NetworkManager/".

This is the recommended way to give a sub-component its own configuration view without duplicating JSON or hard-coding paths.


Persistence (RaftJsonNVS)

RaftJsonNVS is a RaftJson that:

  • Loads its document on construction from an ESP-IDF Non-Volatile Storage (NVS) namespace.
  • Persists changes back to the same namespace when setJsonDoc() is called.
  • Notifies registered listeners through change callbacks.
// Construct against namespace "myMod"; 0 = no length limit
RaftJsonNVS modConfig("myMod", /*jsonMaxlen*/ 4096);

String setting = modConfig.getString("setting", "default");

// Persist a new document (whole-document replacement)
modConfig.setJsonDoc(R"({"setting":"new"})");

Initialisation:

// Once, very early in app startup. RaftCoreApp does this automatically.
RaftJsonNVS::initNVS(/*eraseIfCorrupt*/ true);

Whole-document semantics:

  • setJsonDoc() replaces the entire document for that namespace — there is no in-place patching at the API level. SysMods that want to merge typically read, mutate the resulting String, and write the merged result back.
  • The optional jsonMaxlen constructor argument caps the size of the persisted document; setJsonDoc() returns false if the new document is longer.
  • The NVS key used internally is "JSON" (one document per namespace).

Diagnostics:

RaftJsonNVS::debugShowNVSInfo(/*showContents*/ false);

prints the list of NVS namespaces and their JSON-document sizes via the standard logger.


Change callbacks

RaftJsonNVS (and any other RaftJsonIF implementation that supports mutation) lets you register a callback that fires after a successful setJsonDoc():

modConfig.registerChangeCallback([this]() {
    LOG_I("MyMod", "Configuration changed — reloading");
    reloadFromConfig();
});

Properties:

  • Multiple callbacks may be registered. They are stored as std::function<void()> and invoked in registration order.
  • There is no de-registration API. Choose lifetimes accordingly (typically the listener lives as long as the SysMod).
  • The callback receives no payload — it is a "something changed, please re-read what you care about" signal. Listeners re-fetch values via the normal accessors.
  • Inside a SysMod the convenience method configRegisterChangeCallback() wires this up to whichever config layer the SysManager has assigned to that module.

JSON generation helpers

RaftJson is primarily a reader, but it ships a few static helpers for emitting JSON cheaply.

getJSONFromNVPairs — name/value-pair stitcher

std::vector<RaftJson::NameValuePair> nvp = {
    {"status", "ok"},
    {"count",  "42"},
    {"items",  "[1,2,3]"}      // already-JSON values are passed through unquoted
};
String json = RaftJson::getJSONFromNVPairs(nvp, /*includeOuterBraces*/ true);
// → {"status":"ok","count":"42","items":[1,2,3]}

Rules:

  • Each value whose first non-whitespace character is { or [ is emitted as-is (no quoting). Use this to inline already-encoded objects or arrays.
  • All other values are emitted as JSON strings — including numbers and booleans, which is why the example above shows "count":"42" rather than "count":42. If you need numeric typing, build the value as a literal (e.g. value = "42" and the caller treats the surrounding string as numeric), or assemble the JSON directly.
  • Names are not escaped. Don't use names that contain quotes or backslashes.

escapeString / unescapeString — string-content escaping

String s = "He said \"hi\"\nNew line";
RaftJson::escapeString(s);     // → He said \"hi\"\nNew line
RaftJson::unescapeString(s);   // → He said "hi"\nNew line

The helpers handle three escape sequences: backslash (\\), double-quote (\") and newline (\n). They do not escape control characters in the \u00xx range or \t / \r — if you need full RFC 8259 escaping for arbitrary binary you must do it yourself.

getHTMLQueryFromJSON

String q = RaftJson::getHTMLQueryFromJSON(R"({"a":1,"b":"hi"})");
// → ?a=1&b=hi

Useful when forwarding a JSON body to a legacy URL-query-style endpoint.

getStringWithoutQuotes

When you have a pointer to a JSON element (typically obtained via lower-level helpers or while iterating) and want its raw string form with surrounding quotes stripped and escapes resolved:

String val = RaftJson::getStringWithoutQuotes(pStart, pEnd, /*unescape*/ true);

"Immediate" static accessors

Each instance accessor has a static *Im counterpart that takes the JSON document pointers explicitly:

String s = RaftJson::getStringIm(pJsonDoc, pJsonEnd, "a/b/c", "default");
long   n = RaftJson::getLongIm  (pJsonDoc, pJsonEnd, "a/b/c", -1);
bool   b = RaftJson::getBoolIm  (pJsonDoc, pJsonEnd, "a/b/c", false);
double d = RaftJson::getDoubleIm(pJsonDoc, pJsonEnd, "a/b/c", 0.0);
bool   ok = RaftJson::getKeysIm (pJsonDoc, pJsonEnd, "a/b",   keys);

Use these when you have a JSON buffer that is short-lived (e.g. the body of a REST request) and you want to avoid constructing a RaftJson wrapper. They share the same path syntax and chaining semantics.

RaftJson::getJSONFromRESTRequest builds a RaftJson from the path components of a REST URL (e.g. path[0], path[1], …) — see RaftRestAPIEndpoints for use in API handlers.


Memory and copy semantics

The standard RaftJson constructor takes a makeCopy flag:

RaftJson(const char* pJsonStr, bool makeCopy = true,
         const char* pJsonEnd = nullptr,
         RaftJsonIF* pChainedRaftJson = nullptr);
makeCopy What happens When to use
true (default) The string is copied into a heap allocation owned by the RaftJson. Default. Always safe.
false The RaftJson only stores the pointer. The caller must keep the string alive (and immutable) for the lifetime of the RaftJson. Constant strings in flash (PROGMEM-equivalent), or when wrapping a buffer you already own and outlive the wrapper.

Constructors from String and std::string always copy.

Tips:

  • RaftJsonPrefixed never copies — it wraps a reference.
  • RaftJsonNVS always copies (it owns its document, since it must be able to re-read from NVS).
  • Parse work is on-demand: constructing a RaftJson over a multi-kilobyte document is cheap; the cost is paid per getX call as the relevant subtree is walked.

Common patterns

Per-SysMod config view

// In RaftSysMod-derived setup()
RaftJsonPrefixed myCfg(_modConfig, modName());
int pin = myCfg.getInt("pin", -1);

NV-persisted SysMod state

// In your SysMod
RaftJsonNVS _state{"myMod"};

void setup() override
{
    // Restore state
    _counter = _state.getLong("counter", 0);

    // Track external changes
    _state.registerChangeCallback([this]{ _counter = _state.getLong("counter", _counter); });
}

void persistCounter()
{
    String j = R"({"counter":)" + String(_counter) + "}";
    _state.setJsonDoc(j.c_str());
}

Layered defaults for a sub-system

// Compile-time defaults in flash
static const char* DEFAULTS = R"({"sampleHz":10,"unit":"C"})";
RaftJson defaults(DEFAULTS, /*makeCopy*/ false);

// User override loaded from a file or REST body (caller-owned String)
RaftJson user(userJsonString, /*chained=*/ &defaults);

double hz = user.getDouble("sampleHz", 1.0);   // user wins; default is fallback

See also

Clone this wiki locally