[{"content":"","date":"30 August 2026","externalUrl":null,"permalink":"/docs/espbase/","section":"espbase Documentation","summary":"","title":"espbase Documentation","type":"page"},{"content":" Migrating from ArduinoJson to StackJson # If you are coming from ArduinoJson, you are used to the Document Object Model (DOM) paradigm: allocating a memory pool, building a tree of variants in memory, and then serializing it (or vice versa). StackJson throws out the DOM entirely in favor of a strictly stack-based, zero-copy streaming architecture.\nWhy You\u0026rsquo;d Want To # Zero Dynamic Allocation: No more guessing StaticJsonDocument\u0026lt;N\u0026gt; sizes or worrying about heap fragmentation on your ESP32. Compile-Time Safety: C++20 concepts and fold expressions catch type errors and missing bindings at compile time, instead of failing silently at runtime. Deterministic Memory: Peak RAM usage is exactly the size of your variables and your output buffer. You never hold a duplicate DOM representation in memory. Built for IoT: Perfectly suited for MQTT payloads (like Home Assistant discovery and state topics) where you generate complex JSON on the fly or extract specific keys from massive incoming payloads. The Migration Cheatsheet # Task\rArduinoJson (DOM)\rStackJson (Stream / SAX)\rSimple Object\rStaticJsonDocument\u0026lt;128\u0026gt; doc;\rdoc[\"state\"] = \"ON\";\rdoc[\"brightness\"] = 255;\rserializeJson(doc, buf);\rauto doc = stack_json(\rnode(\"state\", \"ON\"),\rnode(\"brightness\", 255)\r);\rdoc.emit(buf);\rNested Objects\rdoc[\"color\"][\"r\"] = 255;\rdoc[\"color\"][\"g\"] = 128;\rstack_json(\rnode(path(\"color\", \"r\"), 255),\rnode(path(\"color\", \"g\"), 128)\r);\rArrays of Primitives\rJsonArray arr = doc.createNestedArray(\"id\");\rarr.add(\"screenie\");\rarr.add(54);\rstack_json(\rnode(\"id\", stack_array(\"screenie\", 54, ...))\r);\rDynamic Arrays\rstd::vector\u0026lt;int\u0026gt; arr = {42, 54};\rJsonArray arr = doc.createNestedArray(\"id\");\rfor (auto\u0026 i: arr) arr.add(i);\rstd::vector\u0026lt;int\u0026gt; arr = {42, 54};\rstack_json(node(\"id\", span_array(arr)));\rArrays of Objects\rJsonArray arr = doc.createNestedArray(\"sensors\");\rJsonObject obj = arr.createNestedObject();\robj[\"id\"] = 1;\rstack_json(\rnode(\"sensors\", stack_array(\rstack_json(node(\"id\", 1))\r))\r);\rConditional Keys\n(Omit if false)\rif (has_color) {\rdoc[\"color_mode\"] = \"rgb\";\r}\rstack_json(\rnode_if(has_color, \"color_mode\", \"rgb\")\r);\rSafe Null Pointers\rconst char* str = nullptr;\rdoc[\"val\"] = str; // Emits null\rconst char* str = nullptr;\rstack_json(node(\"val\", str));\rBasic Parsing\rdeserializeJson(doc, payload);\rint b = doc[\"brightness\"];\rint b = 0;\rjson_parser(\rbind(\"brightness\", b)\r).parse(payload);\rNested Parsing\rdeserializeJson(doc, payload);\rint r = doc[\"color\"][\"r\"];\rint r = 0;\rjson_parser(\rbind(path(\"color\", \"r\"), r)\r).parse(payload);\rIterating an Array\n(Explicit Binding)\rdeserializeJson(doc, payload);\rfor (int v : doc[\"data\"].as\u0026lt;JsonArray\u0026gt;()) {\rSerial.println(v);\r}\rauto arr_node = bind(\"data\");\rjson_parser(arr_node).parse(payload);\rint v;\rwhile (arr_node \u0026gt;\u0026gt; v) {\rSerial.println(v);\r}\rChecking Null / Types\rdeserializeJson(doc, payload);\rif (doc[\"val\"].isNull()) { ... }\rif (doc[\"val\"].is\u0026lt;int\u0026gt;()) { ... }\rauto val = bind(\"val\");\rjson_parser(val).parse(payload);\rif (val.is_null()) { ... }\rif (val.is_number()) { ... }\rAdvanced: Dynamic Catch-All Parsing # ArduinoJson allows you to arbitrarily loop over documents using for (JsonPair p : doc.as\u0026lt;JsonObject\u0026gt;()).\nBecause StackJson does not build a DOM tree, you cannot natively ask a document for its keys. However, you can achieve the exact same flexibility by passing a callback lambda as the first argument to json_parser. This acts as a \u0026ldquo;catch-all\u0026rdquo; router for any keys you didn\u0026rsquo;t explicitly bind.\nExample: Catching Unknown Keys and Arrays\nauto parser = json_parser( // The Catch-All Callback [\u0026amp;](const PathBase\u0026amp; path, DynamicNodeBase\u0026amp; node) { std::string_view key = path.get_element(path.depth() - 1); if (node.is_array()) { int item; while (node \u0026gt;\u0026gt; item) { printf(\u0026#34;Array %.*s item: %d\\n\u0026#34;, key.size(), key.data(), item); } } else { // Extract the raw JSON substring exactly as captured std::string_view raw = node.raw(); printf(\u0026#34;Found unknown key %.*s: %.*s\\n\u0026#34;, key.size(), key.data(), raw.size(), raw.data()); } }, // Explicit bindings continue to work alongside the callback bind(\u0026#34;known_key\u0026#34;, known_val) ); parser.parse(payload); Unsupported (Out of Scope) # Because StackJson operates strictly as a one-way streaming emitter and a SAX-style sequential parser, it lacks an in-memory document model entirely. The following ArduinoJson patterns are fundamentally incompatible with StackJson\u0026rsquo;s zero-copy architecture:\nIn-Memory Mutation: You cannot do doc[\u0026quot;brightness\u0026quot;] = 100; to update an existing JSON payload. You must emit a fresh payload using your updated C++ variables. Key Deletion: There is no doc.remove(\u0026quot;key\u0026quot;). If you want a key gone, use node_if() so it is never emitted into the buffer in the first place. Deep Copying JSON: You cannot clone a JSON object natively. If you need to manipulate a parsed payload and send it back out, you would use the parser to bind values to native C++ structs, modify the structs, and then serialize them back out using stack_json(). Sorting Keys: StackJson emits keys in the exact order you pass them into the stack_json() parameter pack. ","date":"30 August 2026","externalUrl":null,"permalink":"/docs/espbase/src/espbase/stack_json/migrating_from_arduino_json/","section":"Srcs","summary":"","title":"Migrating from ArduinoJson to StackJson","type":"src"},{"content":"","date":"30 August 2026","externalUrl":null,"permalink":"/docs/espbase/src/","section":"Srcs","summary":"","title":"Srcs","type":"src"},{"content":" StackJson 🧱 # A zero-heap, zero-copy, strictly-typed C++26 JSON library for deeply constrained systems.\nStackJson was built for environments where dynamic memory allocation is a liability—like generating complex MQTT discovery payloads on an ESP32 or parsing deeply nested configuration data inside an RTOS task.\nInstead of allocating a DOM (Document Object Model) on the heap, StackJson uses C++26 template metaprogramming to build a declarative graph of references on your call stack. It computes maximum stack depth requirements at compile time, safely handles Unicode, and achieves perfect zero-copy composition.\n⚡ Motivation # Modern C++ JSON libraries (like nlohmann::json or ArduinoJson) are fantastic, but they rely on dynamic memory. On microcontrollers, constantly allocating and freeing JSON objects leads to memory fragmentation, heap exhaustion, and eventually, a hard crash.\nStackJson flips the model:\nYou own the memory: All data lives in your local variables. The compiler does the work: Stack depths and node relationships are computed at compile time. Zero copies: Documents capture your variables by reference using perfect forwarding. Update your variables after building the document, and the emitted JSON reflects the live changes. 🛠️ Usage Examples: Generation # StackJson allows you to declaratively build JSON payloads that mirror the final output structure. See Migrating from ArduinoJson to StackJson for a side-by-side comparison.\nThe Basics # Paths can be defined inline or stored as variables for clean grouping.\n#include \u0026#34;espbase/stack_json/json.hpp\u0026#34; using namespace sjson; int main() { int battery = 98; auto device = path(\u0026#34;device\u0026#34;); auto doc = stack_json( node(\u0026#34;name\u0026#34;, \u0026#34;espuck_node_01\u0026#34;), node(device(\u0026#34;manufacturer\u0026#34;), \u0026#34;espuck\u0026#34;), node(device(\u0026#34;power\u0026#34;, \u0026#34;battery\u0026#34;), battery), // Deeply nested path node(device(\u0026#34;online\u0026#34;), true) ); StackBuffer\u0026lt;256\u0026gt; buffer; doc.emit(buffer); // buffer.c_str() now contains a null-terminated JSON string! // {\u0026#34;name\u0026#34;:\u0026#34;espuck_node_01\u0026#34;,\u0026#34;device\u0026#34;:{\u0026#34;manufacturer\u0026#34;:\u0026#34;espuck\u0026#34;,\u0026#34;power\u0026#34;:{\u0026#34;battery\u0026#34;:98},\u0026#34;online\u0026#34;:true}} } Conditional Nodes # Need to omit keys if they aren\u0026rsquo;t configured? Use node_if. If the condition is false, the key is completely excluded from the output.\nauto btn_doc = stack_json( node(\u0026#34;command_topic\u0026#34;, \u0026#34;home/cmd\u0026#34;), // conditional =\u0026gt; nulls can be output node_if(config.has_icon, \u0026#34;icon\u0026#34;, config.icon), // no conditional =\u0026gt; nulls drop implicitly node_if(\u0026#34;entity_category\u0026#34;, config.category) ); Arrays and Spans # StackJson handles arrays of primitive types, nested objects, or even runtime spans.\n// Compile-time arrays auto capabilities = stack_array(\u0026#34;wifi\u0026#34;, \u0026#34;ble\u0026#34;, \u0026#34;touch\u0026#34;); // Runtime arrays (wraps a std::span without allocating!) int raw_temps[] = {18, 20, 22, 24}; std::span\u0026lt;int\u0026gt; temps(raw_temps); auto doc = stack_json( node(\u0026#34;features\u0026#34;, capabilities), node(\u0026#34;supported_temps\u0026#34;, span_array(temps)) ); Zero-Copy Composition (Perfect Forwarding) # Because StackJson uses perfect forwarding, assembling a master document from smaller components consumes almost no stack space. The master document simply holds references to the local components.\nauto wifi_doc = stack_json(node(\u0026#34;ip\u0026#34;, \u0026#34;192.168.1.50\u0026#34;)); auto ble_doc = stack_json(node(\u0026#34;mac\u0026#34;, \u0026#34;AA:BB:CC:DD:EE\u0026#34;)); // Outer document captures the inner documents by reference! auto root_doc = stack_json( node(\u0026#34;wifi\u0026#34;, wifi_doc), node(\u0026#34;bluetooth\u0026#34;, ble_doc) ); In-Place String Formatting (Printf Callback) # When generating dynamic strings like MQTT topics or unique IDs, you usually have to waste stack space on temporary buffers (char topic_buf[128]) and call snprintf.\nStackJson eliminates this by accepting a lambda function. It exposes a printf-compatible formatter that writes directly into the unused tail of the OutputBuffer. It even applies JSON escaping in-place using a highly optimized backwards-expansion algorithm—meaning zero intermediate buffers and no std::memmove.\nconst char* device_prefix = \u0026#34;espuck\u0026#34;; const char* sensor_type = \u0026#34;temperature\u0026#34;; auto doc = stack_json( node(\u0026#34;name\u0026#34;, \u0026#34;Kitchen Temp\u0026#34;), // Formats directly into the final JSON payload safely! node(\u0026#34;state_topic\u0026#34;, [\u0026amp;](auto\u0026amp; print) { print(\u0026#34;homeassistant/sensor/%s_%s/state\u0026#34;, device_prefix, sensor_type); }), // Fully supports automatic JSON escaping of your formatted output node(\u0026#34;chaotic_text\u0026#34;, [\u0026amp;](auto\u0026amp; print) { print(\u0026#34;Line 1\\nLine 2 with \\\u0026#34;quotes\\\u0026#34;\u0026#34;); print(\u0026#34; print again - it will get appended\u0026#34;); }) ); The Non-Templated Builder # Need to pass documents across function boundaries or virtual methods? Use StackBuilder to escape the template matrix and merge documents safely on the stack.\nvoid Entity::inject_into(Buffer\u0026amp; buffer, BuilderBase\u0026amp; builder) const { auto base_doc = stack_json(node(\u0026#34;unique_id\u0026#34;, this-\u0026gt;id)); builder.add(base_doc); builder.emit(buffer); // Evaluates all collected documents and writes! } // In the caller: // Space for up to 32 keys (just pointers on the stack). StackBuilder\u0026lt;32\u0026gt; builder; inject_into(buffer, builder); Pretty Printing (Opt-In \u0026amp; Decoupled) # Formatting JSON with newlines and spaces is essential for debugging, but it wastes binary size if forced. StackJson handles this via the Decorator Pattern. You simply wrap your buffer in a PrettyBuffer.\nBecause it\u0026rsquo;s completely decoupled from the generation tree, if you don\u0026rsquo;t use it, the compiler\u0026rsquo;s dead-code elimination (DCE) strips it out entirely!\nStackBuffer\u0026lt;256\u0026gt; raw_buffer; PrettyBuffer pretty(raw_buffer, 2); // 2 spaces per indent doc.emit(pretty); // Emits fully formatted JSON directly into raw_buffer Dynamic Buffers (Heap / PSRAM Support) # When generating massive JSON documents (like Home Assistant MQTT discovery payloads) that exceed your RTOS stack limits, you can use DynamicBuffer.\nDynamicBuffer implements the OutputBuffer interface but backs it with a dynamically growing standard library container. It is fully template-decoupled: it works out-of-the-box with std::vector\u0026lt;char\u0026gt;, but you can easily substitute a custom STL allocator to route memory to an ESP32\u0026rsquo;s SPIRAM/PSRAM.\nIt also automatically maintains a \u0026ldquo;watermark\u0026rdquo; capacity (ExpandSize, defaulting to 256 bytes) at the end of the buffer to guarantee there is always contiguous space available for your zero-copy printf callbacks, without causing out-of-bounds errors.\n// Default routes to ESP32 PSRAM using a custom STL allocator! DynamicBuffer\u0026lt;\u0026gt; psram_buffer; // Or use standard heap allocation... DynamicBuffer\u0026lt;std::vector\u0026lt;char\u0026gt;\u0026gt; heap_buffer; auto doc = stack_json( node(\u0026#34;device_class\u0026#34;, \u0026#34;temperature\u0026#34;), node(\u0026#34;state_topic\u0026#34;, [\u0026amp;](auto\u0026amp; print) { // The buffer guarantees at least 256 bytes are ready for this expansion, // automatically resizing the underlying vector if necessary. print(\u0026#34;home/sensor/%s\u0026#34;, config.identifier); }) ); doc.emit(psram_buffer); // c_str() safely null-terminates and returns a pointer to the vector\u0026#39;s memory mqtt_client.publish(psram_buffer.c_str()); 🔍 Usage Examples: Parsing # StackJson\u0026rsquo;s parser is a zero-allocation SAX engine. You bind your local variables to expected JSON paths, and the parser drops the values right into your variables as it scans.\nType Coercion and Binding # std::string_view json = R\u0026#34;({\u0026#34;state\u0026#34;: \u0026#34;ON\u0026#34;, \u0026#34;brightness\u0026#34;: \u0026#34;50\u0026#34;})\u0026#34;; std::string_view state; int brightness = 0; // The parser will auto-convert the string \u0026#34;50\u0026#34; to an int! auto parser = json_parser( bind(\u0026#34;state\u0026#34;, state), bind(\u0026#34;brightness\u0026#34;, brightness) ); parser.parse(json); Relaxed Parsing (JSONC, Comments \u0026amp; Commas) # IoT config files are often written by humans. StackJson\u0026rsquo;s parser acts like a forgiving JSON5/JSONC engine out of the box. It natively ignores trailing commas and gracefully skips // and /* */ comments.\nstd::string_view json = R\u0026#34;( { // Configure the main port \u0026#34;port\u0026#34;: 8080, \u0026#34;history\u0026#34;: [1, 2, 3,], /* Note the trailing commas! */ } )\u0026#34;; int port = 0; auto parser = json_parser(bind(\u0026#34;port\u0026#34;, port)); parser.parse(json); // Succeeds effortlessly Dynamic Nodes (Variant Parsing, Sub-Parsing \u0026amp; Arrays) # Sometimes you don\u0026rsquo;t know exactly what type a value will be, or you want to parse an array of unknown length. By calling bind() with only a path (no target variable), StackJson creates a DynamicNode.\nA DynamicNode captures the raw, zero-copy std::string_view of the matched JSON block (even full objects or arrays). You can then interrogate its state, extract its value, sub-parse it, or iterate over it.\n1. Variant Extraction \u0026amp; State Tracking # You can check if a node was present in the payload or explicitly set to null before deciding what to do with it.\nauto version = bind(\u0026#34;version\u0026#34;); auto image = bind(\u0026#34;image\u0026#34;); auto not_set = bind(\u0026#34;not_set\u0026#34;); auto parser = json_parser(version, image, not_set); parser.parse(R\u0026#34;({\u0026#34;version\u0026#34;: 11, \u0026#34;image\u0026#34;: \u0026#34;foo.bin\u0026#34;})\u0026#34;); if (image.is_set() \u0026amp;\u0026amp; !image.is_null()) { std::string img; image \u0026gt;\u0026gt; img; // Auto-coerces the captured JSON into your variable } int ver; version \u0026gt;\u0026gt; ver; // Evaluates to true and sets ver to 11 std::string missing; not_set \u0026gt;\u0026gt; missing; // Evaluates to false; missing remains untouched 2. Sub-Parsing (Nested Objects) # If a JSON payload contains a complex nested object, a DynamicNode captures the entire block {...}. You can then spin up a child parser directly on that node to extract its internal fields.\nauto info = bind(path(\u0026#34;project\u0026#34;, \u0026#34;info\u0026#34;)); auto parser = json_parser(info); // Captures the entire \u0026#34;info\u0026#34; object block parser.parse(R\u0026#34;({\u0026#34;project\u0026#34;: {\u0026#34;info\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;espuck\u0026#34;, \u0026#34;tags\u0026#34;: 42}}})\u0026#34;); std::string name; int tags = 0; // Sub-parse the captured block info.parse( bind(\u0026#34;name\u0026#34;, name), bind(\u0026#34;tags\u0026#34;, tags) ); 3. Array Iteration # DynamicNode natively supports array iteration. By using a while(node \u0026gt;\u0026gt; target) loop, it acts as a lightweight tokenizer, extracting one element at a time from the array.\nauto tags = bind(\u0026#34;tags\u0026#34;); auto parser = json_parser(tags); parser.parse(R\u0026#34;({\u0026#34;tags\u0026#34;: [\u0026#34;wifi\u0026#34;, \u0026#34;ble\u0026#34;, \u0026#34;touch\u0026#34;]})\u0026#34;); std::string tag; while (tags \u0026gt;\u0026gt; tag) { // Loops 3 times, yielding \u0026#34;wifi\u0026#34;, \u0026#34;ble\u0026#34;, and \u0026#34;touch\u0026#34; } 4. Object Array Iteration (Zero-Copy!) # By combining array iteration with sub-parsing, you can easily parse arrays of objects without ever allocating a vector or loading the array into memory! Just stream the array elements into a std::string_view (which grabs the raw {...} text of the element), and sub-parse it.\nauto sensors = bind(\u0026#34;sensors\u0026#34;); auto parser = json_parser(sensors); parser.parse(R\u0026#34;({\u0026#34;sensors\u0026#34;: [{\u0026#34;id\u0026#34;: \u0026#34;temp\u0026#34;, \u0026#34;val\u0026#34;: 22.5}, {\u0026#34;id\u0026#34;: \u0026#34;hum\u0026#34;, \u0026#34;val\u0026#34;: 55.0}]})\u0026#34;); std::string_view obj_str; while (sensors \u0026gt;\u0026gt; obj_str) { std::string id; float val = 0; // Parse the individual array element auto sub_parser = json_parser(bind(\u0026#34;id\u0026#34;, id), bind(\u0026#34;val\u0026#34;, val)); sub_parser.parse(obj_str); printf(\u0026#34;Sensor %s: %f\\n\u0026#34;, id.c_str(), val); } Null Tracking of directly-bound nodes # Distinguishing between an omitted key and an explicit null (clear state) for non-dynamic bindings is also possible.\nstd::string_view json = R\u0026#34;({\u0026#34;brightness\u0026#34;: null})\u0026#34;; int brightness = 100; auto brightness_node = bind(\u0026#34;brightness\u0026#34;, brightness); auto parser = json_parser(brightness_node); parser.parse(json); if (brightness_node.was_set()) { if (brightness_node.was_null()) { // Explicitly set to null! // (nulls assign default-initalized values to bindings) } else { // Set to a new value. } } else { // no match =\u0026gt; bindings are untouched. } String Decoding Strategies # Depending on your memory constraints, you can choose how to handle escaped strings (like \\n, \\\u0026quot;, or \\uXXXX sequences):\nstd::string_view: Zero-copy. You get a view directly into the raw, unparsed JSON buffer. std::span\u0026lt;char\u0026gt;: Provide your own stack buffer. StackJson decodes the string (including UTF-8 translation for \\u sequences) into your buffer and safely shrinks the span to the exact decoded length. std::string: The pragmatic escape hatch. StackJson will dynamically resize the heap string to perfectly fit the decoded characters with exactly one allocation. 🔍 Dynamic Parsing \u0026amp; Unknown Keys # StackJson is highly optimized for strict schema binding (extracting exactly the keys you ask for), but you can also dynamically catch and inspect unmapped data. By passing a lambda as the first argument to json_parser, you can create a catch-all callback that routes any unmatched named properties into a custom handler.\nThe callback receives the active path and a DynamicNodeBase object, which provides zero-overhead type identification and extraction methods.\nstd::string_view json = R\u0026#34;({ \u0026#34;known_key\u0026#34;: 42, \u0026#34;dynamic_config\u0026#34;: {\u0026#34;speed\u0026#34;: 100, \u0026#34;mode\u0026#34;: \u0026#34;auto\u0026#34;}, \u0026#34;tags\u0026#34;: [\u0026#34;esp32\u0026#34;, \u0026#34;homeassistant\u0026#34;] })\u0026#34;; int known_val = 0; auto parser = sjson::json_parser( // 1. The Catch-All Callback [\u0026amp;](const sjson::PathBase\u0026amp; path, sjson::DynamicNodeBase\u0026amp; node) { // Get the leaf key name std::string key = std::string(path.get_element(path.depth() - 1)); if (node.is_null()) { printf(\u0026#34;Key \u0026#39;%s\u0026#39; is explicitly null\\n\u0026#34;, key.c_str()); } else if (node.is_array()) { std::string item; // Iterate directly through the array elements while (node \u0026gt;\u0026gt; item) { printf(\u0026#34;Array \u0026#39;%s\u0026#39; contains: %s\\n\u0026#34;, key.c_str(), item.c_str()); } } else if (node.is_object()) { // Grab the raw JSON block without iterating printf(\u0026#34;Object \u0026#39;%s\u0026#39;: %s\\n\u0026#34;, key.c_str(), std::string(node.raw()).c_str()); } else if (node.is_string()) { // .as\u0026lt;T\u0026gt;() safely coerces and unescapes the value printf(\u0026#34;String \u0026#39;%s\u0026#39;: %s\\n\u0026#34;, key.c_str(), node.as\u0026lt;std::string\u0026gt;().c_str()); } else if (node.is_boolean() || node.is_number()) { printf(\u0026#34;Primitive \u0026#39;%s\u0026#39;: %s\\n\u0026#34;, key.c_str(), std::string(node.raw()).c_str()); } }, // 2. Your explicit bindings follow normally sjson::bind(\u0026#34;known_key\u0026#34;, known_val) ); parser.parse(json); DynamicNodeBase Capabilities: # Type Identification: is_null(), is_string(), is_array(), is_object(), is_boolean(), is_number(). .raw(): Returns a std::string_view of the exact, unparsed JSON block captured from the payload. .as\u0026lt;T\u0026gt;(): Quickly extracts and coerces the value into a specific type (e.g., auto s = node.as\u0026lt;std::string\u0026gt;();). operator\u0026gt;\u0026gt;: Use while (node \u0026gt;\u0026gt; val) to sequentially iterate over the elements of an array. 📊 Feature Support Matrix # 🟢 Fully Supported # Zero-Allocation Traversal: Generating and parsing operate completely without the heap. Compile-Time Depth Calculation: Parser bounds checking perfectly auto-sizes to the deepest path requested. UTF-8 / Unicode Decoding: Translates \\uXXXX sequences into valid UTF-8 bytes natively, and passes raw high-byte characters (like °C or emojis) through safely. JSONC Extensions: Natively supports single (//) and multi-line (/* */) comments. Trailing Commas: Safely routes around trailing commas in both objects and arrays. Nested Objects \u0026amp; Paths: Limitless object nesting within the boundaries of your RTOS stack. Auto Type Coercion: Parser seamlessly converts strings to numbers/booleans and vice versa. 🟡 On the Roadmap (TODO) # NDJSON (Newline Delimited JSON): Implementing a streaming reset so the parser can continuously evaluate a stream of back-to-back JSON objects over a serial or TCP socket. 🔴 Out of Scope / Infeasible (The Trade-offs) # Because StackJson operates entirely on the stack and refuses to allocate dynamic memory, we cannot support the following features found in heavy desktop libraries:\nFull DOM Manipulation: We will never load an arbitrary JSON tree into a mutable in-memory DOM. You cannot parse a document, alter a specific nested node, and then re-emit it. Perfect Float Round-Tripping: To save up to 150kB of binary size (by avoiding algorithms like Ryu/Dragonbox), we fall back to \u0026lt;charconv\u0026gt; and \u0026lt;cstdio\u0026gt;. This is fast and small, but may slightly alter float string representations (e.g., 3.14 might emit as 3.1400000000000001). Duplicate Key Detection: If a payload contains {\u0026quot;a\u0026quot;: 1, \u0026quot;a\u0026quot;: 2}, our parser will evaluate the assignment twice, leaving your variable with the last seen value. 🟡 Dynamic / Heap-Based Keys (Supported via LValues) # StackJson does not allocate memory for keys, but it can bind to your dynamically generated keys (e.g., std::string). Thanks to perfect forwarding, the library safely captures a reference to your local string variables. To protect you from dangling memory, a compile-time guardrail will intentionally break the build if you attempt to bind to a temporary/rvalue string.\n🛡️ Safety \u0026amp; Guardrails # StackJson is designed to be highly resilient in embedded environments, prioritizing device uptime and developer experience:\nNull-Pointer Protection: Standard std::string_view implementations will immediately segfault if passed a nullptr. StackJson intercepts runtime nulls. A null value pointer safely emits as a JSON null, and a null path key emits as \u0026quot;null_path_error\u0026quot; to keep your device alive and make debugging obvious. Human-Readable Compiler Errors: If you forget to wrap a payload in node(), or if you attempt to serialize an unsupported custom object, StackJson uses C++20 concepts and static_assert to halt the build with a clear, plain-English explanation of exactly what went wrong. Dangling String Prevention: The library gladly accepts dynamic std::string keys and values via perfect forwarding, but uses compile-time checks to explicitly reject temporary/rvalue strings that would cause use-after-free memory corruption. ","date":"30 August 2026","externalUrl":null,"permalink":"/docs/espbase/src/espbase/stack_json/readme/","section":"Srcs","summary":"","title":"StackJson","type":"src"},{"content":"\rProvides a structured, production-ready C++ environment for ESP-IDF. tapted/espbase wraps FreeRTOS primitives and essential ESP32 subsystems with strict RAII semantics, protecting remote deployments through safe boot mechanisms and crash-loop prevention.\nMechanism # The framework abstracts low-level ESP-IDF C APIs into object-oriented C++ patterns, preventing memory leaks and dangling hardware states. It focuses heavily on execution stability, utilizing delayed power management and rollback watchdogs to ensure the device remains accessible on the network even after a faulty update.\nCore Modules # Module Description Location Boot Boot protection including crash loop detection (check_crash_loop.cpp) and OTA rollback watchdogs (ota_rollback_watchdog.cpp). src/espbase/boot/ Buffers Memory-safe ring buffers for telemetry and state tracking (circular_history_buffer.cpp). src/espbase/ StackJson Stack-Based Non-Allocating JSON Library. src/espbase/stack_json Memory Memory management and allocation targeted at pseudo-static RAM (psram_allocator.h). src/espbase/ NVS RAII-compliant Non-Volatile Storage wrappers for flash persistence (nvs_store.cpp). src/espbase/ Tasks Object-oriented FreeRTOS task management and yielding execution (esp_task.cpp, yielding_task.hpp). src/espbase/ Types Safety wrappers for handling native ESP-IDF esp_err_t responses (esp_result.hpp). src/espbase/ Installation # Add the repository as an ESP-IDF component in your CMakeLists.txt:\nidf_component_register( SRCS \u0026#34;main.cpp\u0026#34; INCLUDE_DIRS \u0026#34;.\u0026#34; REQUIRES espbase ) ","externalUrl":null,"permalink":"/docs/espbase/readme/","section":"espbase Documentation","summary":"","title":"","type":"page"},{"content":" espbase library source directory # This directory contains core hardware abstraction wrappers, network boot utilities, and zero-allocation utility types for the ESP32-S3.\nTechnical Overview # 1. Error Handling (esp_result.hpp) # EspResult\u0026lt;T\u0026gt; and EspError provide a modern C++ type-safe approach to value-or-error propagation, replacing standard output parameters and unsafe return codes.\nEspResultBase: Base class holding the underlying esp_err_t. EspResult\u0026lt;T\u0026gt;: Wrapper representing success (holding value of type T and returning true) or failure (holding error code). Overloaded log_error allows fluent logging chaining, falling back to esp_err_t on lvalues if T is non-copyable/non-movable to preserve safety. EspResult\u0026lt;void\u0026gt;: Specialization for functions returning no value on success. EspError: Lightweight wrapper representing a failed state, providing extraction helpers (EspError::check(...)). .strip(): Discards the success value to retrieve an EspResult\u0026lt;void\u0026gt;, which is recommended when returning/chaining error states for move-only or immovable types. 2. Task Management (esp_task.hpp, yielding_task.hpp) # Placeholder: Core ESP-IDF FreeRTOS task wrappers and cooperative yielding task scheduling implementations.\n3. Non-Volatile Storage (nvs_store.hpp) # Placeholder: Zero-allocation RAII wrappers for ESP-IDF Non-Volatile Storage (NVS).\n4. Memory Allocators (psram_allocator.h) # Placeholder: Custom C++ allocators targeting ESP32 PSRAM.\n5. Serialization (json.h) # Placeholder: Optimized compile-time and runtime JSON serialization utilities.\n6. Boot Utilities (boot/) # Placeholder: Network loggers and boot configuration helpers.\n6. trampoline() (trampoline.hpp) # trampoline() is a compile-time utility that bridges the gap between object-oriented C++ and C-style RTOS APIs. It automatically deduces the class type of a member function and generates a stateless void (*)(void*) callback, completely eliminating the need to write boilerplate static wrapper functions manually.\nExample Usage # class Device { public: void pump_queue() { // Process queue... } void schedule_pump() { // 1. Generate the C-style function pointer trampoline_func_t callback = trampoline\u0026lt;\u0026amp;Device::pump_queue\u0026gt;(); // 2. Execute it by passing the instance pointer callback(this); } }; ","externalUrl":null,"permalink":"/docs/espbase/src/espbase/readme/","section":"Srcs","summary":"","title":"","type":"src"},{"content":"","externalUrl":null,"permalink":"/docs/espbase/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/docs/espbase/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/docs/espbase/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/docs/espbase/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"}]