The Verdict: Sizing Your JsonDocument in 2026

For any new ESP32 project parsing HTTP payloads, default to ArduinoJson v7 and use the unified JsonDocument class. The manual capacity math and heap-fragmentation nightmares of v6’s StaticJsonDocument and DynamicJsonDocument are obsolete. Version 7 automatically grows its memory pool on the heap, eliminating the most common cause of silent parsing failures: under-allocating buffer space.

Decision Path: Which ArduinoJson version and class should you use?
  • If you are writing new code for ESP32/ESP8266 in 2026 Install ArduinoJson v7.x and use JsonDocument.
  • If you are maintaining a legacy codebase that fails to compile on v7 Stick to v6.21.x and use DynamicJsonDocument for payloads over 1KB, StaticJsonDocument for tiny, fixed payloads under 512 bytes.
  • If your payload exceeds 200KB Abandon ArduinoJson. Use a SAX-style streaming parser like StreamJson or write a custom regex extractor to avoid exhausting the ESP32’s SRAM.

While v7 handles capacity dynamically, you still need to manage how the data enters the parser. Passing a massive HTTP string directly into deserializeJson() will spike your heap usage and trigger a watchdog reset. The solution is stream-based parsing, which we will implement in the complete sketch below.

Hardware BOM and ESP32 Pin Mapping

This build targets the most common hobbyist board: the ESP32-WROOM-32 DevKit V1 (30-pin or 38-pin variant). We are fetching live GitHub repository stats and rendering them on a standard 0.96″ SSD1306 I2C OLED. Total hardware cost is typically under $12.

ComponentExact Variant / SpecESP32 GPIO Pin
MicrocontrollerESP32-WROOM-32 DevKit V1 (30-pin)N/A
Display0.96″ SSD1306 I2C OLED (128x64, 4-pin)VCC=3V3, GND=GND
I2C Data (SDA)Jumper Wire (Female-to-Female)GPIO 21
I2C Clock (SCL)Jumper Wire (Female-to-Female)GPIO 22

Note: On the 38-pin DevKit V1 variant, SDA is often on GPIO 21 and SCL on GPIO 22 as well, but always verify your specific board’s silkscreen. Never power the OLED from the VIN/5V pin if your module lacks a 5V-to-3.3V onboard regulator; use the 3V3 pin to prevent logic-level damage to the ESP32’s I2C bus.

The Complete ESP32 JSON Fetch and Parse Sketch

This code targets ArduinoJson v7. It connects to WiFi, queries the GitHub API for the official ArduinoJson repository stats, parses the stream directly (avoiding heap duplication), and renders the star count on the OLED. Error handling is built into both the HTTP and JSON layers.

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- Pin Definitions & Hardware Config ---
#define I2C_SDA 21
#define I2C_SCL 22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

// --- Network & API Config ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Fetching stats for the ArduinoJson repo itself
const char* jsonUrl = "https://api.github.com/repos/bblanchon/ArduinoJson";

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(115200);
  delay(500);

  // Initialize I2C with explicit pins to avoid default remapping issues
  Wire.begin(I2C_SDA, I2C_SCL);

  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Halt execution
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("Connecting WiFi...");
  display.display();

  WiFi.begin(ssid, password);
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 40) {
    delay(500);
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nWiFi Connected.");
  } else {
    Serial.println("\nWiFi Failed.");
    display.clearDisplay();
    display.setCursor(0,0);
    display.println("WiFi Failed!");
    display.display();
  }
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    delay(5000);
    return;
  }

  HTTPClient http;
  http.begin(jsonUrl);
  // GitHub API requires a User-Agent header, or it returns 403 Forbidden
  http.addHeader("User-Agent", "ESP32-ArduinoJson-Bot");
  
  int httpCode = http.GET();

  if (httpCode == HTTP_CODE_OK) {
    // v7 JsonDocument automatically manages heap memory
    JsonDocument doc;
    
    // Parse directly from the HTTP stream to save RAM
    DeserializationError error = deserializeJson(doc, http.getStream());

    if (error) {
      Serial.print("deserializeJson() failed: ");
      Serial.println(error.f_str());
      displayError(error.f_str());
    } else {
      // Extract nested values safely
      int stars = doc["stargazers_count"] | 0; // Fallback to 0 if missing
      int issues = doc["open_issues_count"] | 0;
      
      Serial.printf("Stars: %d | Issues: %d\n", stars, issues);
      renderData(stars, issues);
    }
  } else {
    Serial.printf("HTTP GET failed, error: %s\n", http.errorToString(httpCode).c_str());
    displayError(http.errorToString(httpCode).c_str());
  }

  http.end();
  delay(60000); // Fetch once per minute to respect API rate limits
}

void renderData(int stars, int issues) {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.setTextSize(1);
  display.println("ArduinoJson Repo");
  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
  
  display.setTextSize(2);
  display.setCursor(0, 16);
  display.print("Stars:");
  display.setCursor(0, 36);
  display.println(stars);
  
  display.setTextSize(1);
  display.setCursor(0, 54);
  display.print("Open Issues: ");
  display.print(issues);
  display.display();
}

void displayError(const char* errMsg) {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println("ERROR:");
  display.println(errMsg);
  display.display();
}

Debugging DeserializationError: The First Three Checks

When your serial monitor spits out a DeserializationError, do not immediately increase buffer sizes or rewrite your JSON. The ESP32 HTTP stack and ArduinoJson interact in specific ways that generate predictable errors. Here are the first three things to check, ranked by frequency.

1. The Exact Error: "IncompleteInput"

The Cause: The HTTP stream terminated before the JSON parser found the closing brace } or bracket ]. This almost always happens when the server uses Transfer-Encoding: chunked and the ESP32’s HTTPClient drops the connection before the final zero-length chunk is processed, or when your WiFi signal drops mid-stream.

The Fix: If you are using http.getStream(), ensure you call http.setTimeout(5000) before http.GET() to give the stream reader enough time to assemble chunked boundaries. If the payload is small (<10KB), bypass the stream entirely and use String payload = http.getString(); deserializeJson(doc, payload);.

2. The Exact Error: "InvalidInput"

The Cause: The parser encountered bytes that violate the JSON RFC (e.g., unescaped quotes, trailing commas, or single quotes instead of double quotes). On the ESP32, this also frequently occurs when an HTTP API returns an HTML error page (like a 403 Forbidden or 502 Bad Gateway gateway page) instead of JSON, and you didn’t check the httpCode before passing the stream to the parser.

The Fix: Always wrap deserializeJson in an if (httpCode == HTTP_CODE_OK) block. If you suspect malformed data, use the ArduinoJson DeserializationError decoder or print the raw stream to the serial monitor to verify you aren’t accidentally parsing an HTML <head> tag.

3. The Exact Error: "NoMemory" (v6) or Heap Reboots (v7)

The Cause: In ArduinoJson v6, this meant your StaticJsonDocument capacity was too small. In v7, JsonDocument grows automatically, but if you attempt to parse a 500KB JSON payload, the ESP32’s contiguous heap will fragment, allocation will fail, and the ESP32 will throw a Guru Meditation Error: Core 1 panic'ed (LoadProhibited) and reboot.

The Fix: Use ArduinoJson’s filtering feature (detailed below) to ignore irrelevant keys, or switch to a SAX parser for payloads exceeding your available RAM (typically ~200KB on a standard ESP32-WROOM).

Extending the Build: JSON Filtering and RAM Conservation

API payloads are notoriously bloated. The GitHub API response for a repository contains over 80 keys, including nested objects for permissions, license data, and organization metadata. Parsing the entire document into a JsonDocument wastes precious SRAM.

To extend this build for memory-constrained environments (like the ESP8266 or when running concurrent TLS connections), use a JsonDocument filter. This tells the parser to drop ignored keys during deserialization, drastically reducing the memory footprint.

// Create a filter document
JsonDocument filter;
filter["stargazers_count"] = true;
filter["open_issues_count"] = true;
// All other keys are implicitly false and will be discarded

JsonDocument doc;
DeserializationError error = deserializeJson(doc, http.getStream(), DeserializationOption::Filter(filter));

By applying this filter, a 15KB GitHub payload shrinks to a few hundred bytes in RAM, completely eliminating heap fragmentation risks and speeding up the parsing loop by orders of magnitude.

Final Recommendation: When to Drop JSON Entirely

ArduinoJson is the undisputed standard for C++ embedded parsing, but it is not a universal solution. Do not use JSON if your payload exceeds 250KB or if you are operating on an ATmega328P (Arduino Uno/Nano) with strict 2KB RAM limits.

For massive telemetry payloads, switch to MessagePack (which ArduinoJson supports natively via deserializeMsgPack()) or Protobuf. For simple key-value telemetry on legacy AVRs, abandon structured parsing entirely and use delimited CSV strings (temp:24.5,hum:60) with the standard strtok() C function. However, for 95% of ESP32 IoT dashboard and API-fetching projects in 2026, ArduinoJson v7 with stream-based parsing and filtering remains the definitive, most robust choice.