Why does the generated JSON document contain garbage?
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
.