Description of the problem

You called serializeJson() and expected to get the following output:

{"hello":"world"}

but instead, you got:

{"hello":"�"}

or any other kind of strange output.

Why does this happen?

Garbage in the output is the sign of dangling pointers, i.e., pointers to destructed variables.

This problem occurs when:

  1. The JsonDocument is constructed with variables that are destroyed before the call to serializeJson() (examples 1 and 2).
  2. A JsonArray, JsonObject, or JsonVariant refers to a destructed JsonDocument (example 3).

Example 1: destructed string value

The following program fills a JsonDocument with a temporary String:

// DON'T DO THAT!!!  💀
doc["address"] = address.toString().c_str();
serializeJson(doc, Serial);  // <- likely to produce garbage

The problem is that the call to address.toString() produce a temporary String that is destructed as soon as the line is executed.

By calling String::c_str(), the program gets a pointer to the temporary string and gives it to ArduinoJson. Since ArduinoJson sees a const char*, it doesn’t duplicate the string and simply saves the pointer.

The problem can be avoided by removing the call to String::c_str():

doc["address"] = address.toString(); // <- duplicates
serializeJson(doc, Serial);

Now, ArduinoJson sees a String and knows that it needs to make a copy of the string in the JsonDocument.

Example 2: destructed input string

The following function deserializes a JSON document using the “zero-copy” mode, but doesn’t keep the input in memory:

void loadConfig(JsonDocument& doc){
  File file = SPIFFS.open("config.json", "r");

  // DON'T DO THAT!!!  💀
  size_t size = file.size();
  std::unique_ptr<char[]> buf (new char[size]);
  file.readBytes(buf.get(), size);
  deserializeJson(doc, buf.get());

  file.close();
}

Indeed, when called with a char* (or a char[]), deserializeJson() uses the zero-copy mode.
In this mode, the JsonDocument stores pointers to bytes in the input.
The zero-copy mode is very efficient, but it requires that the input buffer has a longer lifetime than the JsonDocument.

To fix this function, just change the type of input to something that is read-only. In this particular case, it’s possible to pass the file directly:

void loadConfig(JsonDocument& doc){
  File file = SPIFFS.open("config.json", "r");
  deserializeJson(doc, file);
  file.close();
}

Now, ArduinoJson duplicates relevant pieces of the input in the JsonDocument.

If unlike this example, your input is not a Stream but a plain old char*, you can force ArduinoJson to make a copy by casting the pointer to a const char*:

deserializeJson(doc, (const char*)input);

Example 3: destructed JsonDocument

The following program creates a JsonObject from a temporary JsonDocument.

// DON'T DO THAT!!!  💀
JsonObject createObject() {
  StaticJsonDocument<200> doc;
  JsonObject obj = doc.to<JsonObject>();
  obj["hello"] = "world";
  return obj;
}

The JsonObject returned by this function points to a destructed JsonDocument, and therefore is likely to produce garbage or crash the program.

The best way to fix this function is to pass the JsonDocument as an argument:

JsonObject createObject(JsonDocument& doc) {
  JsonObject obj = doc.to<JsonObject>();
  obj["hello"] = "world";
  return obj;
}

ESP only: garbage after booting the board

If you try to run the ArduinoJson examples on an ESP8266 or an ESP32, you’ll see something like this in the Serial Monitor:

!,▒▒bf4HXa>@%▒gps
1351824120
48.756081
2.302038

As you can see, the output of the program is preceded by some random characters.

This is an entirely different issue that is not related to ArduinoJson. When an ESP boots, it prints some information to the serial port. The problem is that it uses a different baud rate, so the Serial Monitor cannot decode the data. This initial baud rate might change from one board to another, but it seems to be either 74880 or 115200.

For example, on my Adafruit HUZZAH (which contains an ESP8266), it’s 74880. If I change the baud rate in both the program and the Serial Monitor, I get the following output:


 ets Jan  8 2013,rst cause:2, boot mode:(3,7)

load 0x4010f000, len 1392, room 16 
tail 0
chksum 0xd0
csum 0xd0
v3d128e5c
~ld
gps
1351824120
48.756081
2.302038

On my generic ESP32 board, however, I must use 115200 baud to get the following output:

ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0018,len:4
load:0x3fff001c,len:1216
ho 0 tail 12 room 4
load:0x40078000,len:9720
ho 0 tail 12 room 4
load:0x40080400,len:6352
entry 0x400806b8
gps
1351824120
48.756081
2.302038

You can find some information here:

See also