Description

You called JsonObject::printTo() 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 always has the same cause: the JsonObject contains pointers to destructed variables.

This problem happens when the JsonObject is constructed with variables that are destroyed before the call to printTo()

Example 1: the JsonBuffer is destructed

The following program creates a JsonObject from a temporary JsonBuffer. The problem is that the JsonBuffer is destructed as soon as the function returns, so the reference points to a destructed variable.

// DON'T DO THAT!!!
JsonObject& createObject() {
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& obj = jsonBuffer.createObject();
  obj["hello"] = "world";
  return obj;
}

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

template<typename TJsonBuffer>
JsonObject& createObject(TJsonBuffer& jsonBuffer) {
  JsonObject& obj = jsonBuffer.createObject();
  obj["hello"] = "world";
  return obj;
}

Note that this function uses a template to allow any kind of JsonBuffer to be used, not just StaticJsonBuffer<200>.

Example 2: destructed string

The following program fills a JsonObject with a temporary String:

// DON'T DO THAT!!!
JsonObject& obj = jsonBuffer.createObject();
obj["address"] = address.toString().c_str();
obj.printTo(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():

JsonObject& obj = jsonBuffer.createObject();
obj["address"] = address.toString(); // <- duplicates
obj.printTo(Serial);

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

Example 3: destructed input in zero-copy mode

The following function uses the zero-copy mode, but doesn’t keep the input in memory:

JsonObject&  loadPlastic(DynamicJsonBuffer& jsonBuffer){
  File file = SPIFFS.open(HISTORY_FILE, "r");

  // DON'T DO THAT!!!
  size_t size = file.size();
  std::unique_ptr<char[]> buf (new char[size]);
  file.readBytes(buf.get(), size);
  JsonObject& root = jsonBuffer.parseObject(buf.get());

  file.close();
  return root;
}

Indeed, when called with a char* (or a char[]), JsonBuffer::parseObject() uses the zero-copy mode. In this mode, the JsonObject stores pointers to bytes in the input.

The zero-copy mode is very efficient, but it requires that the input variable has a longer lifetime than the JsonObject.

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

JsonObject&  loadPlastic(DynamicJsonBuffer& jsonBuffer){
  File file = SPIFFS.open(HISTORY_FILE, "r");
  JsonObject& root = jsonBuffer.parseObject(file);
  file.close();
  return root;
}

Now, ArduinoJson will duplicates the relevant pieces of the input in the JsonBuffer.