Repository navigation
RaftJsonAdvanced
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.
| 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.
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); // detachNotes:
- 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. -
getKeyson 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, whena/bexists in the primary butconly in the fallback, will not fall through to the fallback (the path is considered "found, but noc"). 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.
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.
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 resultingString, and write the merged result back. - The optional
jsonMaxlenconstructor argument caps the size of the persisted document;setJsonDoc()returnsfalseif 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.
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.
RaftJson is primarily a reader, but it ships a few static helpers for emitting JSON cheaply.
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
valuewhose 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.
String s = "He said \"hi\"\nNew line";
RaftJson::escapeString(s); // → He said \"hi\"\nNew line
RaftJson::unescapeString(s); // → He said "hi"\nNew lineThe 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.
String q = RaftJson::getHTMLQueryFromJSON(R"({"a":1,"b":"hi"})");
// → ?a=1&b=hiUseful when forwarding a JSON body to a legacy URL-query-style endpoint.
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);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.
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:
-
RaftJsonPrefixednever copies — it wraps a reference. -
RaftJsonNVSalways copies (it owns its document, since it must be able to re-read from NVS). - Parse work is on-demand: constructing a
RaftJsonover a multi-kilobyte document is cheap; the cost is paid pergetXcall as the relevant subtree is walked.
// In RaftSysMod-derived setup()
RaftJsonPrefixed myCfg(_modConfig, modName());
int pin = myCfg.getInt("pin", -1);// 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());
}// 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- JSON / RaftJson — basics and path syntax.
-
Configuration — how the chained layers are wired in
RaftCoreApp. -
Helper Classes Index — one-line index of related utilities (
ArPreferences,NamedValueProvider). - SysTypes — where the SysType layer comes from.
- Source:
components/core/RaftJson/.
Getting Started
- Quick Start
- Architecture at a Glance
- Writing Your First SysMod
- Adding a Comms Channel
- Adding an I2C Device Type
- PlatformIO / Arduino
Scaffolding & Building
- Raft CLI
- SysTypes
- Top-Level SysType
- Build Process
- WebUI Build Pipeline
- File System
- Partitions & Flash
- Local Dev Libraries
- Library Developer Guide
Architecture
Built-in SysMods
- NetworkManager
- WiFi Troubleshooting
- BLEManager
- WebServer
- MQTTManager
- SerialConsole
- CommandSerial
- CommandSocket
- CommandFile
- FileManager
- LogManager
- ESPOTAUpdate
- StatePublisher
- Remote Logging
- Data Source Registration
Comms & Protocols
- Stack Overview
- Comms Channels
- ProtocolExchange
- RICREST Protocol
- Real-Time Streams
- Adding REST Endpoints
- Built-in REST Endpoints
- File Download (OKTO)
- OTA Update Flow
Devices & Buses
- DeviceManager
- Device Manager REST API
- Device Factory & Classes
- Device Type Records
- Adding an I2C Device Type
- Device Data Publishing
- Data Logger
- I2C Bus
- I2C Device Scanning
- I2C ID & Polling
- MotorControl Overview
- MotorControl Config
- MotorControl Commands
ROS 2
Helpers
Reference