This page explains how to use ArduinoJson with PubSubClient, a popular MQTT library for Arduino. It shows how to use the JSON format in MQTT messages, but you can easily adapt the examples to use MessagePack instead.

Deserializing a JSON document in MQTT message

Basic usage

Once your program has subscribed to an MQTT topic, you can call deserializeJson() from the callback function.

void callback(char* topic, byte* payload, unsigned int length) {
  StaticJsonDocument<256> doc;
  deserializeJson(doc, payload, length);
  // use the JsonDocument as usual...
}

With a global JsonDocument

Because we pass a non-const pointer to deserializeJson(), it will use the zero-copy mode: instead of copying strings into the JsonDocument, it will store pointers. On the one hand, it’s excellent for performance and memory consumption; but on the other hand, it can create dangling pointers.

You can disable the zero-copy mode by casting payload to a pointer-to-const. This is helpful if, for example, you use a global JsonDocument, like so:

StaticJsonDocument<256> doc;

void callback(char* topic, byte* payload, unsigned int length) {
  // cast payload to pointer-to-const to disable ArduinoJson's zero-copy mode
  deserializeJson(doc, (const byte*)payload, length);
}

However, remember that there are some caveats from using a global JsonDocument.

Avoiding dangling pointers

Even if you don’t use a global JsonDocument, the callback is a great place to create dangling pointers. Make sure you never store a pointer that you acquired in the callback:

// BAD EXAMPLE: DON'T DO THAT!!!
const char *name = NULL;

void callback(char* topic, byte* payload, unsigned int length) {
  StaticJsonDocument<256> doc;
  deserializeJson(doc, payload, length);
  name = doc["name"]; // 💀 pointer is invalid as soon as function returns
}

Instead, make a copy of the string:

const char name[32] = "";

void callback(char* topic, byte* payload, unsigned int length) {
  StaticJsonDocument<256> doc;
  deserializeJson(doc, payload, length);
  strlcpy(name, doc["name"] | "default", sizeof(name));
}

Of course, you can use a String if you prefer.

Serializing a JSON document into an MQTT message

To publish a JSON document to an MQTT topic, you need to serialize it to a temporary buffer:

char buffer[256];
serializeJson(doc, buffer);
client.publish("outTopic", buffer);

You can save a few CPU cycles by passing the size of the payload to publish():

char buffer[256];
size_t n = serializeJson(doc, buffer);
client.publish("outTopic", buffer, n);

By default, PubSubClient limits the message size to 256 bytes (including header); see the documentation.

Can we avoid the temporary buffer?

It’s tempting to remove the temporary buffer to save some memory. For example, we can write:

client.beginPublish(topic, measureJson(doc), retained);
serializeJson(doc, client);
client.endPublish();

However, this code is much slower than the one with a temporary buffer (100 to 200 times slower from our experience).

This slowness is due to the Client class that sends bytes one by one. To improve the speed, we need to insert a small buffer as shown in How to improve (de)serialization speed?:

client.beginPublish(topic, measureJson(doc), retained);
BufferingPrint bufferedClient(client, 32);
serializeJson(doc, bufferedClient);
bufferedClient.flush();
client.endPublish();

Notice that I used a BufferingPrint instead of WriteBufferingClient because, despite its name, the PubSubClient class doesn’t implement the Client but the Print interface. For more information on BufferingPrint, see the README of the StreamUtils library.

This works, but as you can see, it’s much more complicated than the original code, so I don’t think it’s worth the effort.

See also

Global warming stripes by Professor Ed Hawkins (University of Reading)