Project Overview & Hardware Spec Sheet

When building dynamic IoT systems, hardcoding GPIO configurations is a dead end. You need a way to pass a list of hardware configurations at runtime. This is where parsing Arduino JsonArray members becomes critical. In this guide, we will build a dynamic GPIO configurator that reads a JSON array of relay states, validates the pins against a hardware whitelist, and sets the outputs accordingly.

Difficulty: Intermediate | Time: 45 Minutes | Target Board: ESP32 DevKit V1 (NodeMCU-32S 38-pin variant)

Parts List

  • Microcontroller: ESP32 DevKit V1 (38-pin NodeMCU-32S variant) — $6.00 - $9.00
  • Logic Level Converter: TXS0108E 8-channel bi-directional module — $2.50
  • Relays: 3x Songle SRD-05VDC-SL-C 5V active-low relay modules — $4.00 each
  • Wiring: 22 AWG solid core jumper wires, 1x half-size breadboard

Pin Mapping Table

The ESP32 operates at 3.3V logic, while the Songle relay modules require 5V logic to trigger reliably without chattering. We use the TXS0108E to shift the levels. Bench note: Ensure the OE (Output Enable) pin on the TXS0108E is tied directly to 3.3V, or the outputs will float.

ESP32 GPIO TXS0108E (LV Side) TXS0108E (HV Side) Relay Module Function
GPIO 16 A1 B1 IN1 Pump Control
GPIO 17 A2 B2 IN2 Valve Control
GPIO 18 A3 B3 IN3 Heater Control
3V3 VCCA & OE - - Low Voltage Ref
5V (VIN) - VCCB VCC High Voltage Ref

Understanding Arduino JsonArray Members vs. Elements

A common point of confusion in the ArduinoJson V7 documentation is the terminology. Strictly speaking, a JsonArray contains elements (indexed by position), while a JsonObject contains members (key-value pairs). When makers search for "Arduino JsonArray members," they are almost always trying to iterate through an array of objects and access the object members nested inside each array element.

For example, in the payload [{"pin": 16, "state": "HIGH"}], the entire object {"pin": 16, "state": "HIGH"} is the element at index 0. The key "pin" and its value 16 are the members of that object. ArduinoJson V7 handles this elegantly with range-based for loops, automatically casting the element to a JsonObject so you can extract the members directly.

Pro Tip: Unlike V6, which required you to pre-calculate memory using DynamicJsonDocument(2048), ArduinoJson V7 uses a JsonDocument that automatically allocates memory from the heap in blocks. This prevents the classic ESP32 stack overflow crashes when parsing large arrays of sensor configurations.

Complete ESP32 Code: Parsing Array Members

This code targets the ESP32 DevKit V1. It defines a hardware whitelist to prevent the JSON payload from accidentally configuring strapping pins (like GPIO 0 or GPIO 2) which would cause boot failures.

#include <ArduinoJson.h>
#include <cstring>

// Target: ESP32 DevKit V1 (NodeMCU-32S 38-pin variant)
// Hardware: 3x Songle SRD-05VDC-SL-C Relays via TXS0108E Logic Level Converter

#define MAX_CONFIGURABLE_PINS 4
const int ALLOWED_PINS[MAX_CONFIGURABLE_PINS] = {16, 17, 18, 19};

const char* json_config = R"([
  {"id": 1, "pin": 16, "state": "HIGH", "label": "Pump"},
  {"id": 2, "pin": 17, "state": "LOW", "label": "Valve"},
  {"id": 3, "pin": 18, "state": "HIGH", "label": "Heater"},
  {"id": 4, "pin": 2, "state": "HIGH", "label": "BadPin"}
])";

bool isPinAllowed(int pin) {
  for (int i = 0; i < MAX_CONFIGURABLE_PINS; i++) {
    if (ALLOWED_PINS[i] == pin) return true;
  }
  return false;
}

void setup() {
  Serial.begin(115200);
  while (!Serial) { delay(10); }
  Serial.println(F("Initializing GPIO Configurator..."));

  // V7 Syntax: JsonDocument replaces DynamicJsonDocument
  JsonDocument doc; 
  DeserializationError error = deserializeJson(doc, json_config);

  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }

  JsonArray configArray = doc.as<JsonArray>();
  
  // Iterating through array elements to access nested object members
  for (JsonObject element : configArray) {
    int id = element["id"];
    int pin = element["pin"];
    const char* stateStr = element["state"];
    const char* label = element["label"];

    // Safety check: Do not configure ESP32 strapping pins
    if (!isPinAllowed(pin)) {
      Serial.printf("[REJECTED] ID %d (%s): GPIO %d is not in the allowed hardware map.\n", id, label, pin);
      continue;
    }

    pinMode(pin, OUTPUT);
    int stateVal = (strcmp(stateStr, "HIGH") == 0) ? HIGH : LOW;
    digitalWrite(pin, stateVal);
    
    Serial.printf("[OK] Configured ID %d (%s) on GPIO %d to %s\n", id, label, pin, stateStr);
  }
}

void loop() {
  // Static configuration loaded in setup. 
  // Add your main application logic here.
  delay(1000);
}

Debugging: Exact Error Strings & Ranked Causes

When working with Arduino JsonArray members, compilation and runtime errors are common, especially when migrating from older tutorials. If your build fails, look for these exact strings.

Error 1: error: 'class ArduinoJson::V720PB2::JsonArray' has no member named 'createNestedObject'

Ranked Causes:

  1. V6 to V7 API Migration: You are using ArduinoJson V7, but following a V6 tutorial. V7 removed createNestedObject().
  2. Fix: Replace array.createNestedObject() with array.add<JsonObject>().

Error 2: error: 'DynamicJsonDocument' was not declared in this scope

Ranked Causes:

  1. Library Version Mismatch: DynamicJsonDocument was deprecated and removed in V7.
  2. Fix: Change DynamicJsonDocument doc(2048); to simply JsonDocument doc;.

The First Three Things to Check When Parsing Fails

If the code compiles but deserializeJson() returns an error at runtime, check these immediately:

  1. Trailing Commas: JSON strictly forbids trailing commas in arrays or objects (e.g., [{"pin": 16},]). Run your payload through a linter like JSONLint.
  2. String Escaping: If reading from an SD card or HTTP, ensure internal quotes are escaped properly. Using raw string literals R"(...)" in C++ avoids this during testing.
  3. Memory Fragmentation: While V7 handles memory better, parsing a massive 50KB array on an ESP32 without PSRAM can still fail with NoMemory. If your array is huge, ensure you are using an ESP32-WROVER module with external PSRAM and configure ArduinoJson to use it.

Extending and Simplifying the Build

How to Extend: To make this a true IoT device, replace the hardcoded json_config string with an HTTP GET request using the ESP32's HTTPClient library. You can stream the JSON directly into the JsonDocument using deserializeJson(doc, http.getStream()). This avoids loading the entire payload into RAM twice, a critical optimization for ESP32 memory-constrained environments.

How to Simplify: If you just want to test the parsing logic without wiring up relays and logic level converters, strip out the pinMode and digitalWrite calls. Keep the Serial.printf statements to verify that the array elements and object members are being extracted correctly. You can run this simplified version on any Arduino Uno or Nano by changing the serial baud rate to 9600.

FAQ: Arduino JsonArray Members

How do I count the number of members in an Arduino JsonArray?

Technically, arrays have elements, not members. To count the number of elements in a JsonArray, use the size() method: int count = configArray.size();. If you need to count the key-value pairs (members) inside a specific object within that array, you cast the element to a JsonObject and call size() on that object.

Can I modify JsonArray members in place without copying?

Yes. ArduinoJson V7 uses reference semantics. When you iterate through the array using for (JsonObject element : configArray), element is a reference to the actual data inside the JsonDocument. If you execute element["state"] = "LOW";, you are modifying the underlying document directly, not a copy. This saves significant RAM on microcontrollers.

Why does iterating over JsonArray members crash my ESP32?

The most common cause of an ESP32 crash (Guru Meditation Error / Stack Overflow) during iteration is attempting to access a member that doesn't exist, which returns a null variant, and then passing that null variant to a function that expects a strict type (like strcmp). Always use element["key"].isNull() or provide default values using the pipe operator: const char* state = element["state"] | "LOW"; to prevent null pointer dereferences.

What is the difference between JsonArray elements and JsonObject members?

An element is a value inside an array, accessed by a zero-based integer index (e.g., array[0]). A member is a key-value pair inside an object, accessed by a string key (e.g., object["pin"]). When you have an array of objects, you first select the element by index, then access the member by key: array[0]["pin"].