The Evolution of Array Handling in ArduinoJson v7
When building IoT telemetry nodes, sensor gateways, or smart home devices, transmitting batched data is a fundamental requirement. In the Arduino ecosystem, the ArduinoJson library remains the undisputed industry standard for serialization and deserialization. With the widespread adoption of ArduinoJson v7 across the maker and professional engineering communities, understanding the Arduino JSON JsonArray object is critical for writing robust, memory-safe firmware.
Unlike standard C++ arrays or std::vector, a JsonArray in ArduinoJson is not a standalone container. It is a reference type—a lightweight proxy that points to a specific node within the memory pool of a JsonDocument. This architectural distinction dictates how you allocate memory, iterate through elements, and handle edge cases on resource-constrained microcontrollers.
What is an Arduino JSON JsonArray?
A JsonArray represents an ordered sequence of JSON values. These values can be primitives (integers, floats, booleans), strings, or nested objects and arrays. Because it is a reference type, creating a JsonArray variable does not allocate new memory for the array structure itself; it merely creates a pointer to the underlying data managed by the JsonDocument.
DynamicJsonDocument and StaticJsonDocument. In v7, this has been unified into a single JsonDocument class that automatically manages heap allocation. Consequently, your JsonArray references remain valid as long as the parent JsonDocument exists and is not explicitly cleared or destroyed.
Reference vs. Const Reference
ArduinoJson v7 enforces strict mutability rules. If you parse an incoming JSON payload, you receive a JsonArrayConst, which is read-only. To modify or build an outgoing payload, you must explicitly cast or create a mutable JsonArray.
JsonArray: Read/write access. Used when constructing payloads or modifying parsed data.JsonArrayConst: Read-only access. Used when parsing incoming HTTP/MQTT payloads to save RAM and prevent accidental mutations.
Memory Architecture and Microcontroller Constraints
The most common point of failure when working with an Arduino JSON JsonArray is memory exhaustion. Every element added to a JsonArray consumes a variant node in the JsonDocument's memory pool. On 32-bit architectures like the ESP32, each variant node consumes roughly 16 bytes, plus the memory required for string duplication.
Safe JsonDocument Sizing by Architecture
Blindly allocating large JSON documents leads to heap fragmentation and spontaneous reboots, particularly on the ESP8266 and ESP32. The table below outlines safe operational limits for JSON arrays based on Espressif memory allocation guidelines and AVR SRAM limits.
| Microcontroller | Total SRAM | Max Safe JsonDocument | Max Array Elements (Approx) |
|---|---|---|---|
| ATmega328P (Uno/Nano) | 2 KB | ~800 Bytes | ~30 (primitives) |
| ESP8266 (NodeMCU) | ~50 KB (usable) | 16 KB | ~600 (primitives) |
| ESP32 (Standard) | 520 KB | 64 KB | ~2,500 (primitives) |
| ESP32-S3 / RPi Pico | 512 KB+ / 264 KB | 128 KB / 32 KB | ~5,000 / ~1,200 |
Step-by-Step: Creating and Populating a JsonArray
To build an outgoing telemetry payload, you must initialize the JsonDocument, create the array reference, and populate it. In ArduinoJson v7, the add() method returns a boolean indicating whether the allocation succeeded.
#include <ArduinoJson.h>
void buildTelemetryPayload() {
JsonDocument doc;
// Create a mutable JsonArray reference
JsonArray sensorArray = doc["readings"].to<JsonArray>();
// Populate the array with sensor data
bool success1 = sensorArray.add(23.5); // Temperature
bool success2 = sensorArray.add(45.2); // Humidity
// Check for memory allocation failures
if (!success1 || !success2 || doc.overflowed()) {
Serial.println("Error: JsonDocument capacity exceeded!");
return;
}
// Serialize to Serial
serializeJson(doc, Serial);
}
Handling Nested Objects within Arrays
Often, an Arduino JSON JsonArray must hold complex objects rather than simple primitives. You can append a new JsonObject directly to the array using add<JsonObject>().
JsonArray devices = doc["devices"].to<JsonArray>();
JsonObject dev1 = devices.add<JsonObject>();
dev1["id"] = "sensor_01";
dev1["status"] = "online";
Iterating Through an Arduino JSON JsonArray
When parsing incoming configuration files or MQTT commands, you must iterate through the array safely. ArduinoJson v7 fully supports modern C++ range-based for loops, making iteration clean and type-safe. As noted in the C++ language reference for range-based loops, this syntax relies on underlying iterators, which ArduinoJson implements natively for both JsonArray and JsonArrayConst.
void parseIncomingConfig(const char* payload) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload);
if (error) {
Serial.print("Deserialize failed: ");
Serial.println(error.f_str());
return;
}
// Extract as read-only const array
JsonArrayConst ipList = doc["allowed_ips"].as<JsonArrayConst>();
// Iterate using JsonVariantConst
for (JsonVariantConst ip : ipList) {
if (ip.is<const char*>()) {
Serial.print("Allowing IP: ");
Serial.println(ip.as<const char*>());
}
}
}
Edge Cases and Common Failure Modes
Working with dynamic memory on microcontrollers introduces several edge cases that can cause silent data corruption or hard faults. Be vigilant about the following scenarios:
1. Orphaned References
Because a JsonArray is just a pointer to the JsonDocument's internal pool, destroying or reassigning the JsonDocument invalidates the array. Accessing an orphaned JsonArray will result in undefined behavior or a Guru Meditation Error (ESP32 panic).
Rule of Thumb: Never return aJsonArrayfrom a function if theJsonDocumentwas declared locally within that function. Always pass theJsonDocumentby reference or serialize the data before the function scope ends.
2. DeserializationError::NoMemory
When parsing a massive JSON string from an HTTP client, the JsonDocument may run out of heap space. Unlike v6, where you had to pre-calculate capacity using the ArduinoJson Assistant, v7 dynamically allocates memory. However, if the heap is fragmented, allocation will fail. Always check doc.overflowed() or inspect the DeserializationError enum immediately after calling deserializeJson().
3. String Duplication Overhead
When adding strings to an Arduino JSON JsonArray, the library duplicates the string into the document's memory pool by default. If you are adding string literals or global constants that will outlive the JsonDocument, use JsonString::Static or wrap the literal to prevent unnecessary RAM duplication:
// Forces the library to store a pointer instead of duplicating the string
sensorArray.add(JsonString("static_sensor_name", JsonString::Static));
Real-World Telemetry Batching Strategy
For battery-operated ESP32 nodes using deep sleep, transmitting data every second is inefficient. A common pattern is to store readings in a local array, and upon waking, format them into an Arduino JSON JsonArray for a single MQTT publish.
To optimize this, keep the JsonDocument scoped tightly around the transmission event. Do not keep the document alive during the sensor polling phase. Instead, store raw primitives in a standard C++ array or std::vector in RAM, and only map them to a JsonArray milliseconds before serialization. This minimizes heap fragmentation and ensures the ESP32's Wi-Fi radio has maximum contiguous memory available for the TLS handshake.
Summary
Mastering the Arduino JSON JsonArray requires shifting your mindset from standard C++ containers to proxy-based memory management. By respecting the boundaries of the JsonDocument, utilizing const-correctness for parsing, and monitoring heap limits on your specific microcontroller, you can build highly reliable IoT firmware capable of handling complex, nested data structures without triggering memory faults.
