ESP32 and Arduino

ESP32 ArduinoJson MQTT: Send and Parse JSON Payloads the Right Way

Serialize sensor readings and parse incoming JSON commands with ArduinoJson 7 on an ESP32, publish them over MQTT with PubSubClient, and avoid the buffer and memory pitfalls that silently drop messages.

ESP32 board publishing a JSON document built with ArduinoJson in an ESP32 ArduinoJson MQTT tutorial
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Combining ESP32, ArduinoJson and MQTT is the most common way to send structured sensor data from a microcontroller. In this tutorial you will build JSON telemetry with ArduinoJson 7, publish it efficiently with PubSubClient, parse JSON commands in the message callback and learn the buffer limits that cause messages to vanish without an error.

Why JSON Payloads for MQTT

MQTT does not care what is inside a payload; it moves bytes. That freedom means you have to choose a format. JSON wins for most ESP32 projects because every dashboard, Node-RED flow, Python script and database importer can read it without a custom decoder. A message like {"temp":22.4,"hum":51,"rssi":-63} is self-describing, easy to extend with new fields, and readable in any MQTT client while you debug.

The costs are size and parsing work, both negligible at typical sensor rates on an ESP32. If you are sending hundreds of messages per second or running on a tight battery budget, compact keys help more than switching formats.

ArduinoJson is the de facto library for this. Version 7 removed the need to pick a document capacity: JsonDocument grows as needed on the heap. All code below targets ArduinoJson 7; if you are on version 6, replace JsonDocument with StaticJsonDocument<N> or DynamicJsonDocument. The official ArduinoJson documentation covers migration details.

What You Need

  • An ESP32 board and the Arduino IDE or PlatformIO.
  • Libraries: PubSubClient by Nick O'Leary and ArduinoJson by Benoit Blanchon (7.x).
  • mosquitto_pub and mosquitto_sub on your computer to send commands and watch output.

The examples use the public area of broker.connectmq.com on port 1883 with the topic prefix connectmq/demo/a8e2d1. Change that prefix to your own random string, and remember that public topics are readable by anyone.

Topic Direction Payload
connectmq/demo/a8e2d1/telemetry ESP32 publishes {"device":"...","temp":22.4,"hum":51,"uptime":120}
connectmq/demo/a8e2d1/cmd ESP32 subscribes {"action":"set_interval","value":10}
connectmq/demo/a8e2d1/ack ESP32 publishes {"ok":true,"action":"set_interval"}

Complete ESP32 ArduinoJson MQTT Sketch

The sketch publishes telemetry on a configurable interval and accepts JSON commands to change that interval or toggle an LED.

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* TOPIC_TELEMETRY = "connectmq/demo/a8e2d1/telemetry";
const char* TOPIC_CMD       = "connectmq/demo/a8e2d1/cmd";
const char* TOPIC_ACK       = "connectmq/demo/a8e2d1/ack";
const int LED_PIN = 2;

WiFiClient net;
PubSubClient mqtt(net);
String deviceId;

unsigned long intervalMs = 5000;
unsigned long lastPublish = 0;

void publishAck(const char* action, bool ok, const char* error = nullptr) {
  JsonDocument doc;
  doc["ok"] = ok;
  doc["action"] = action;
  if (error) doc["error"] = error;

  char buf[128];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(TOPIC_ACK, (const uint8_t*)buf, n, false);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  DeserializationError err = deserializeJson(doc, payload, length);
  if (err) {
    publishAck("unknown", false, err.c_str());
    return;
  }

  const char* action = doc["action"] | "";
  if (strcmp(action, "set_interval") == 0) {
    int seconds = doc["value"] | 0;
    if (seconds < 1 || seconds > 3600) {
      publishAck(action, false, "value out of range");
      return;
    }
    intervalMs = seconds * 1000UL;
    publishAck(action, true);
  } else if (strcmp(action, "led") == 0) {
    bool on = doc["value"] | false;
    digitalWrite(LED_PIN, on ? HIGH : LOW);
    publishAck(action, true);
  } else {
    publishAck(action, false, "unknown action");
  }
}

void publishTelemetry() {
  JsonDocument doc;
  doc["device"] = deviceId;
  doc["temp"] = serialized(String(20.0 + random(0, 50) / 10.0, 1));
  doc["hum"] = 45 + random(0, 15);
  doc["uptime"] = millis() / 1000;

  JsonObject network = doc["net"].to<JsonObject>();
  network["rssi"] = WiFi.RSSI();
  network["ip"] = WiFi.localIP().toString();

  size_t len = measureJson(doc);
  mqtt.beginPublish(TOPIC_TELEMETRY, len, false);
  serializeJson(doc, mqtt);       // stream straight into the MQTT packet
  mqtt.endPublish();
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(deviceId.c_str())) {
      mqtt.subscribe(TOPIC_CMD);
    } else {
      Serial.printf("MQTT failed, state=%d\n", mqtt.state());
      delay(3000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);

  deviceId = "esp32-json-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setBufferSize(1024);       // default is 256 bytes, too small for most JSON
  mqtt.setCallback(onMessage);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (millis() - lastPublish >= intervalMs) {
    lastPublish = millis();
    publishTelemetry();
  }
}

Watch the output and send commands from your computer:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-a8e2d1 -t 'connectmq/demo/a8e2d1/#' -v

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
  -t connectmq/demo/a8e2d1/cmd -m '{"action":"set_interval","value":10}'

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
  -t connectmq/demo/a8e2d1/cmd -m '{"action":"led","value":true}'

Serializing: Three Ways to Get JSON Into a Publish

Into a Fixed Buffer

serializeJson(doc, buf, sizeof(buf)) writes into a char array and returns the number of bytes written. This is the simplest and works well for small, predictable messages like the acknowledgement above. Pass the returned length to publish(), using the overload that takes a byte pointer and a length, rather than relying on strlen().

Streaming Into the MQTT Client

PubSubClient inherits from Arduino's Print class, so ArduinoJson can write directly into it. measureJson(doc) gives the exact size first, beginPublish() writes the MQTT header with that size, serializeJson(doc, mqtt) streams the body and endPublish() finishes the packet. No intermediate buffer is needed, which is the most memory-efficient option for larger documents. You still need setBufferSize() for regular publish() calls and for incoming messages, which are always read into the buffer.

Into a String

serializeJson(doc, someString) is convenient but allocates on the heap for every message. On a device that runs for weeks, repeated String allocations can fragment memory. Prefer the first two options in long-running firmware.

Controlling Float Precision

ArduinoJson prints floats with enough digits to round-trip, which can give values like 22.399999. Wrapping a formatted string in serialized() inserts it verbatim, as in the telemetry function above. Alternatively, round the number before assigning it: doc["temp"] = round(t * 10) / 10.0;.

Parsing JSON Commands Safely

The MQTT callback receives a byte pointer and a length, and the payload is not null-terminated. Always pass the length: deserializeJson(doc, payload, length). Calling it with only (char*)payload reads past the end of the message.

Three more habits make parsing robust:

  1. Check the error. DeserializationError tells you whether the input was empty, incomplete, invalid or too deep. Report it back on an ack topic, as the sketch does, so the sender knows the command failed.
  2. Use defaults with the | operator. doc["value"] | 0 returns 0 if the field is missing or has the wrong type, so a malformed command cannot crash the device or leave a variable uninitialised.
  3. Validate ranges. JSON parsing succeeding does not mean the values make sense. Reject intervals of zero seconds or brightness values of 5000 explicitly.

If you only need a few fields from a large incoming document, use a filter so ArduinoJson ignores the rest:

JsonDocument filter;
filter["action"] = true;
filter["value"] = true;

JsonDocument doc;
deserializeJson(doc, payload, length, DeserializationOption::Filter(filter));

Buffer and Memory Pitfalls

Symptom Cause Fix
publish() returns false, nothing arrives Packet larger than PubSubClient's 256-byte default buffer mqtt.setBufferSize(1024) before connect()
Incoming JSON never reaches the callback Incoming message larger than the buffer Same: raise the buffer to fit the largest expected command
IncompleteInput errors Parsing with strlen() on a non-terminated payload Pass length to deserializeJson()
Random reboots after days Heap fragmentation from String-based serialisation Serialize into fixed buffers or stream into the client
Values print with long decimals Default float formatting serialized() or rounding before assignment

Keep the broker limits in mind as well. The public area of broker.connectmq.com accepts up to 64 KB per message, and the free ConnectMQ Serverless plan up to 256 KB, which is far beyond what an ESP32 sketch should send in one message. If your JSON is getting large, split it into topics such as .../telemetry and .../diagnostics. The PubSubClient buffer size guide explains the buffer arithmetic in detail.

Designing the JSON Schema

A few conventions save trouble once more than one device and more than one consumer are involved:

  • Include a device ID or put it in the topic, not both inconsistently.
  • Use units in key names (temp_c, power_w) or document them once; do not mix units across devices.
  • Add a timestamp only if the device has synced time. Otherwise let the consumer stamp the arrival time; see adding NTP timestamps to ESP32 MQTT messages.
  • Keep commands and telemetry on separate topics so a device never parses its own messages.
  • Version the payload with a field like "v":1 if the format might change.

Using It With Real Sensors

Replace the random values with actual readings and the rest of the sketch stays the same. The ESP32 DHT22 MQTT tutorial shows the sensor side, and the ESP32 IoT tutorial assembles a full device project around the same JSON pattern.

Send Your JSON Telemetry to ConnectMQ

Testing against the free public MQTT broker is the fastest way to iterate on a payload format. When the data matters, move to a private broker: ConnectMQ is a fully managed MQTT service whose portal shows your JSON messages live in the topic explorer and lets you build threshold alert rules on numeric fields. Sign up for the free Serverless plan at app.connectmq.com, no credit card required, then switch the sketch to WiFiClientSecure on port 8883 with YOUR_CLIENT_ID, YOUR_USERNAME and YOUR_PASSWORD.

Frequently Asked Questions

Why does PubSubClient fail to publish my JSON?

The default buffer is 256 bytes, including the topic and header. Call mqtt.setBufferSize() with a larger value before connecting, and check the return value of publish().

Should I use ArduinoJson 6 or 7?

Use version 7 for new projects. It replaces fixed-capacity documents with a single JsonDocument that sizes itself, which removes a common source of truncated output.

Is JSON too heavy for an ESP32 over MQTT?

No. At typical sensor rates the parsing and serialisation cost is tiny. Keep keys short and payloads focused if you are on battery or sending many messages per second.

How do I parse the payload when it is not null-terminated?

Pass the length to the parser: deserializeJson(doc, payload, length). ArduinoJson then reads exactly the bytes of the message.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.