How to use ArduinoJson with PubSubClient?
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.