-
Notifications
You must be signed in to change notification settings - Fork 0
-
Part 1 - Common questions
- Compilation fails? Device crashes? Nothing on serial console?
- Why does my device crash or reboot?
- What are the differences between
StaticJsonBufferandDynamicJsonBuffer? - How to determine the buffer size?
- What are the common sizes for JsonBuffer?
- I found a memory leak in the library!
- Why shouldn't I use a global
JsonBuffer? - How to reuse a
JsonBuffer? - What's the best way to use the library?
- How to write a function that works with both
JsonArrayandJsonObject? - How to assign a
JsonArrayorJsonObject?
- Part 2 - Serialization questions
-
Part 3 - Deserialization/parsing questions
- Can I parse data from a stream?
- Can I parse a JSON input that is too big to fit in memory?
- Should I call
parseArray\(\)orparseObject\(\)? - Why parsing fails?
- The first parsing succeeds, why do the next ones fail?
- Parsing succeeds but I can't read the values!
- How to know the type of a value?
- Can I access to object member by its index, instead of its key?
- How to fix error "Ambiguous overload for 'operator='"
See [Compatibility issues](Compatibility issues) first.
99.999% of the time, this is caused by a "stack overflow", i.e. you have too many variables in the "stack". The solution is to move variables to the "heap".
First, replace the StaticJsonBuffer by a DynamicJsonBuffer.
Then, use dynamic allocation for the JSON input.
For instance, if you have a program like this:
char content[MAX_CONTENT_SIZE];
StaticJsonBuffer<JSON_BUFFER_SIZE> jsonBuffer;
receive(content);
JsonObject& root = jsonBuffer.parseObject(content);
Serial.println(root["name"].asString());you should transform it like that:
char* content = malloc(MAX_CONTENT_SIZE);
DynamicJsonBuffer jsonBuffer(JSON_BUFFER_SIZE);
receive(content);
JsonObject& root = jsonBuffer.parseObject(content);
Serial.println(root["name"].asString());
free(content);Of course, this is only possible if your target has enough memory. Sometimes, it's just impossible because the MCU is too small.
See issues #233, #234 and #262
| `StaticJsonBuffer` | `DynamicJsonBuffer`
---------------- | ------------------- | -------------------
Size | fixed | variable 👍
Location | stack
(1)
(2) there is a workaround (see How to reuse a JsonBuffer? if you are looking for troubles).
(3) A DynamicJsonBuffer calls malloc() to allocate its memory, and it may have to do this several times if it needs to grow. However, you can specify an initial size to the constructor, so as to make sure that the buffer is big enough and that no further allocation will be needed.
ℹ️ As a general rule, if your StaticJsonBuffer is bigger than 2KB, then it may be a good time to switch to a DynamicJsonBuffer.
There are basically tree approaches here:
- either you can predict the content of the object tree,
- you know how much memory is available.
- you try and look at current size
In the first case, you know some constraints on the object tree. For instance, let's say that you know in advance (and by that I mean "at compilation time") that you want to generate an object with 3 values, one of them being an array with 2 values, like the following:
{"sensor":"gps","time":1351824120,"data":[48.756080,2.302038]}
To determine the memory usage of this object tree, you use the two macros JSON_ARRAY_SIZE(n) and JSON_OBJECT_SIZE(n), both take the number of elements as an argument.
For the example above, it would be:
const int BUFFER_SIZE = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2);
StaticJsonBuffer<BUFFER_SIZE> jsonBuffer;
You can use the JsonBuffer size calculator to get the values for common platforms.
In the second case, let's say you dynamically generate a JSON object tree of a random complexity so you can't put a limit based on that. But on the other hand, you don't want your program to crash because the object tree doesn't fit in memory. The solution here is to determine how much memory is available, or in other words how much memory you can afford for the JSON object tree.
The third solution is to run your program an print jsonBuffer.size() to get the current size of the buffer.
WARNING 1: if you pass a String or a const char* to parseArray() or parseObject, the JsonBuffer will make a copy of the input, so it will need to be significantly bigger.
WARNING 2: if you use String to create your JSON keys or values, their content will automatically be duplicated in the JsonBuffer, so you need to add the total length of all strings in the size of the JsonBuffer.
| Input | AVR 8-bit | ESP8266 | x86 | x64 |
|---|---|---|---|---|
| OpenWeatherMap (one location) | 436 | 712 | 1384 | 1504 |
| Weather Underground (one location) | 882 | 1424 | 2816 | 2912 |
| Forecast.io | 13442 | 21588 | 42648 | 44232 |
Theses results were generated with the JsonBuffer size calculator.
This is very unlikely. You're probably using the library incorrectly.
The typical problem comes from reusing a JsonBuffer several time.
Each time you call parseArray(), parseObject(), createArray() and createObject(), you consume memory in the JsonBuffer.
To avoid running out of memory, you should discard unused data as soon as possible.
The recommended practice is to do the JSON handling in a dedicated function, with a local JsonBuffer that will be automatically reclaimed when the function exits.
This means that you cannot return a JsonArray or a JsonObject from that function, because they would contains dangling pointers to what used to be the JsonBuffer.
Instead, you should convert the content of the JsonArray to a custom array or vector; or the content of the JsonObject to your own data structure.
This seems like a constraint, but remember that you're programming for an embedded platform with very limited resources, and that requires special techniques.
A positive side effect of following this recommendation is that the code is safe and memory efficient. It also encourages the separation of responsibilities: the function is only in charge of the JSON serialization and no specific JSON data leaks elsewhere is the program.
See issues #72, #87, #115, #135
See ArduinoJson: Avoiding pitfalls and FAQ: What's the best way to use the library
ArduinoJson is designed to do one thing and to do it well: the JSON serialization.
It's not a framework that you can use as a backbone of your application.
In particular JsonObject and JsonArray should not replace your own data structures; that would be terribly inefficient.
So before trying to use a global JsonBuffer, ask yourself first "Am I really using ArduinoJson for serializing, or am I pushing it beyond its initial intent?".
See also How to reuse a JsonBuffer? and What's the best way to use the library?.
See issues #160, #203, #219, #242, #243, #341 and #347.
StaticJsonBuffer and DynamicJsonBuffer are designed to be throwaway memory pools, they are not intended to be reused.
As a consequence, using a global JsonBuffer is almost always a bad idea.
If you believe you need to reuse a JsonBuffer, it's because you're not using the library correctly.
Imagine a clear() function is available, someone could write:
JsonObject& myObject = jsonBuffer.createObject();
jsonBuffer.clear();
JsonArray& myArray = jsonBuffer.createArray();And in this case myObject and myArray would point to the exact same location.
Therefore, any modification of one would corrupt the other.
That's why StaticJsonBuffer and DynamicJsonBuffer have been design to force you to use them in a scope.
If you believe you need a clear() function, then you're not using the library correctly.
Destructing an constructing a new StaticJsonBuffer is not expensive, it's exactly the same cost as the clear() function above.
You can find a very simple workaround in the Bag of tricks.
This will never be a part of the library.
See issues #72, #115, #141 and #242
See ArduinoJson: Avoiding pitfalls
Here is the canonical example for serializing and deserializing with ArduinoJson.
By following this example, you are making the best usage of your memory and you maintain a good software design.
struct SensorData {
const char* name;
int time;
float value;
};
#define SENSORDATA_JSON_SIZE (JSON_OBJECT_SIZE(3))
bool deserialize(SensorData& data, char* json)
{
StaticJsonBuffer<SENSORDATA_JSON_SIZE> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(json);
data.name = root["name"];
data.time = root["time"];
data.value = root["value"];
return root.success();
}
void serialize(const SensorData& data, char* json, size_t maxSize)
{
StaticJsonBuffer<SENSORDATA_JSON_SIZE> jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
root["name"] = data.name;
root["time"] = data.time;
root["value"] = data.value;
root.printTo(json, maxSize);
}As you can see the StaticJsonBuffer is kept in memory as short as possible, so that the remain of your program is unaffected by the JSON serialization.
Also you can see that neither JsonArray nor JsonObject leak out of the serialization code. This maintain a good isolation and reduce the coupling with the library.
There is no base class for JsonArray and JsonObject.
(Back in version 3.0, they used to derive from Printable, but this inheritance has been removed to reduce the memory footprint.)
However, both JsonArray and JsonObject can be "stored" in a JsonVariant. (I put "stored" in quotes because the JsonVariant only contains a reference, not a copy.)
So here is your function:
void sendJson(JsonVariant json) {
char buffer[512];
json.printTo(buffer, sizeof(buffer));
g_server.send(200, "application/json", buffer);
}But in that case, you loose some specificities of the JsonObject class.
In particular, you don't have the containsKey() method.
If you need this function, you must cast the JsonVariant back to a JsonObject&.
For instance:
void myFunction(JsonVariant variant)
{
if (variant.is<JsonArray&>())
{
JsonArray& array = variant;
// ...
}
else if (variant.is<JsonObject&>())
{
JsonObject& object = variant;
// ...
}
else
{
// ...
}
}If you try to reassign a JsonArray& or a JsonObject&, you'll have the following error:
error: use of deleted function 'ArduinoJson::JsonArray& ArduinoJson::JsonArray::operator=(const ArduinoJson::JsonArray&)'
error: use of deleted function 'ArduinoJson::JsonObject& ArduinoJson::JsonObject::operator=(const ArduinoJson::JsonObject&)'
Indeed, you cannot reassign a JsonObject&.
One solution is to use a pointer instead.
JsonObject* myObject = &root["myObject"].as<JsonObject>();You can also use a JsonVariant which will act as a wrapper around the pointer.
JsonVariant myObject = root["myObject"];Use measureLength() to compute the number of characters that will be printed by printTo().
Use measurePrettyLength() to compute the number of characters that will be printed by prettyPrintTo().
None of these methods include the zero-terminator. So if you need to allocate a buffer, don't forget to add 1 to the size.
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
root["hello"] = world;
size_t len = root.measureLength(); // returns 17
size_t size = len+1;
char json[size];
root.printTo(json, size); // writes {"hello":"world"}See issues #75, #94, #166, #178 and #268.
To create a nested object, call createNestedObject().
To create a nested array, call createNestedArray().
For example:
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
JsonObject& weather = root.createNestedObject("weather");
weather["temperature"] = 12;
weather["condition"] = "cloudy";
JsonArray& coords = root.createNestedArray("coords");
coords.add(48.7507371, 7);
coords.add(2.2625587, 7);
root.prettyPrintTo(Serial);will generate:
{
"weather": {
"temperature": 12,
"condition": "cloudy"
},
"coords": [
48.7507371,
2.2625587
]
}This is usually caused by a reused JsonBuffer.
The solution is simply to NOT reuse the JsonBuffer.
See The first parsing succeeds, why do the next ones fail?
First of all, ArduinoJson is not slow by itself. It's slow when used in conjunction with the WifiClient from the ESP8266 core.
The problem is that there is no buffer between ArduinoJson and the WifiClient.
To solve this, either:
- Enable the Nagle algorithm on
WifiClientby callingsetNoDelay(false). - Serialize to a buffer and send the whole buffer in one shot.
- Insert a BufferedPrint proxy between ArduinoJson and
WifiClient.
No.
This is a fundamental design principle in this library. The JSON input must be in memory and must be mutable (i.e., not read-only) to allow zero-copy and zero-allocation, which is the strength of this library.
Let's see an example to understand why this is important:
char json[] = "{\"hello\":\"world\"}";
JsonObject& root = jsonBuffer.parseObject(json);
const char* world = root["hello"];After executing the lines above, the variable world will point to the word "world" inside the json string. During the call to parseObject(), the json string has been modified to insert the necessary zero-terminator (\0), to cut the string world.
As you can see this process requires neither duplication nor allocation, but imposes the input to be stored in a char[].
To parse data from a stream, you'll have to read its content and put it in a char[]:
#define MAX_JSON_SIZE 256
char json[MAX_SIZE];
stream.readBytesUntil('\n', json, MAX_JSON_SIZE);If this is not acceptable, you should have a look at other libraries, like json-streaming-parser.
See issues #60, #119, #194, #200 and #223.
This is a very common question as people are often confused when the JSON input contains mixed arrays and objects.
The answer is very simple: it's the type of the root that matters.
This means that you just need to look at the first character: it's either a [, for an array, or a {, for an object.
For example, if the input is:
{"mickey":["mouse"],"donald":["duck"]}then you must call parseObject() because the root is an object.
And, if the input is:
[{"mickey":"mouse","donald":"duck"}]then you must call parseArray() because the root is an array.
Finally, if you cannot know in advance the type of the root, please open an issue. Don't worry this can be implemented very easily, it's just that nobody asked for it.
The parsing functions, parseArray() and parseObject(), may fail for 4 reasons:
- The input is not a valid JSON
- The
StaticJsonBufferis too small - The
StaticJsonBufferis too big (stack overflow) - The
DynamicJsonBufferfails to allocate memory
Case 1. seems obvious, but a lot of issues are caused by from an invalid input. In particular, if you're writing an HTTP client, you need to skip the HTTP headers and send only the JSON payload to ArduinoJson. See issues #108, #167, #218 and #237
If you're in case 2., you can solve the problem by increasing the size of the StaticJsonBuffer or by switching to a DynamicJsonBuffer.
See issues #53, #89, #202, #280, #293 and #296
If you're in case 3., you can try to switch to a DynamicJsonBuffer. Indeed, most platforms have a fixed size of the stack (usually 4KB on the ESP8266) the rest of the RAM being reserved to the heap. That's why moving the JsonBuffer from the stack to the heap can solve this kind of problem.
See issues #167 and #234.
If you're in case 4., you may have a memory leak, it up to you to find it. You can try to switch to StaticJsonBuffer which is more efficient.
Also, if the input string is constant, the JsonBuffer will have to make a copy of it.
// case 1: char array => no duplication => good
char[] json = "{\"hello\":\"world\"}";
jsonBuffer.parseObject(json);
// case 2: const char* => duplication => bad
const char* json = "{\"hello\":\"world\"}";
jsonBuffer.parseObject(json);
// case 3: String => duplication => bad
String json = "{\"hello\":\"world\"}";
jsonBuffer.parseObject(json);To avoid any duplication, make sure you use an input of type char* or char[]
See issues #154, #177, #179 and #223 and #320.
This can be due to two causes.
First, this can happen if you reuse the same JsonBuffer, for example:
StaticJsonBuffer<200> jsonBuffer;
for (int i=0; i<10; i++) {
char json[256];
readJsonFromSomewhere(json, sizeof(json));
JsonObject& root = jsonBuffer.parse(json);
if (root.success()) {
Serial.println("parseObject() succeeded");
} else {
Serial.println("parseObject() failed!");
}
}The solution is simply to NOT reuse the JsonBuffer, like this:
for (int i=0; i<10; i++) {
char json[256];
readJsonFromSomewhere(json, sizeof(json));
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parse(json);
if (root.success()) {
Serial.println("parseObject() succeeded");
} else {
Serial.println("parseObject() failed!");
}
}Note that, contrary to a common belief, moving a StaticJsonBuffer inside of a loop has no negative impact on performance.
In order to make the JSON parsing without any allocation or duplication, ArduinoJson modifies the string in place: it inserts null terminators and unescapes special characters.
If you provide a writeable input, like a char[] or a char*, it will modify this input in place.
If you provide a read only input, like a const char* or a String, it will have to make a copy of it in order to be allowed to modify it.
That's why it's highly recommended to used a writeable input: you get a huge performance boost and memory usage is greatly reduced 👍
Now, this behavior leads to unexpected result if you try to reuse the modified string, for instance:
char json[256];
readJsonFromSomewhere(json, sizeof(json));
for (int i=0; i<10; i++) {
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parse(json);
if (root.success()) {
Serial.println("parseObject() succeeded");
} else {
Serial.println("parseObject() failed!");
}
}Only the first call to parseObject() will succeed because after that call, json will be altered and not be valid JSON anymore.
The solution is simply to parse the input only once, or get a fresh input at each iteration:
for (int i=0; i<10; i++) {
char json[256];
readJsonFromSomewhere(json, sizeof(json));
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parse(json);
if (root.success()) {
Serial.println("parseObject() succeeded");
} else {
Serial.println("parseObject() failed!");
}
}See issue #153, #160 and #286.
99.999% of the time, this is caused by a confusion between arrays and objects.
This often happens when the JSON contains [{ or :[.
[{"hello":"world"}]Wrong implementation:
JsonObject& root = jsonBuffer.parseObject(json);
const char* world = root["hello"];Good implementation:
JsonArray& root = jsonBuffer.parseArray(json);
const char* world = root[0]["hello"];{"hello":["world"]}Wrong implementation:
JsonObject& root = jsonBuffer.parseObject(json);
const char* world = root["hello"];Good implementation:
JsonArray& root = jsonBuffer.parseArray(json);
const char* world = root["hello"][0];{"hello":[{"new":"world"}]}Wrong implementation:
JsonObject& root = jsonBuffer.parseObject(json);
const char* world = root["hello"]["new"];Good implementation:
JsonArray& root = jsonBuffer.parseArray(json);
const char* world = root["hello"][0]["new"];See issues #187, #203 and #245.
JsonVariant, which is the type that hold the values in JsonArray and JsonObject, provides the method is<T>() that returns true if the value is of type T.
Examples:
object["key"] = "value";
object["key"].is<const char*>(); // returns true
object["key"].is<int>(); // returns false
object["key"].is<JsonObject&>(); // returns false
array.add(42);
array[0].is<const char*>(); // return false
array[0].is<int>(); // return true
array[0].is<JsonObject&>(); // return falseSee issues #148, #175 and #213.
No.
But you can enumerate all the key-value pairs in the object, by using iterators:
char json[] = "{\"key\":\"value\"}";
JsonObject& object = jsonBuffer.parseObject(json);
for(JsonObject::iterator it=object.begin(); it!=object.end(); ++it)
{
const char* key = it->key;
if (it->value.is<char*>()) {
const char* value = it->value;
// ...
}
if (it->value.is<JsonObject>()) {
JsonObject& value = it->value;
// you can recursively traverse objects...
}
}See issue #278.
There is a case where you need to help the compiler: it's when you convert a JsonVariant to a String.
For example:
String ssid = network["ssid"];
ssid = network["ssid"];The first line will compile but the second will fail with the following error:
error: ambiguous overload for 'operator=' (operand types are 'String' and 'ArduinoJson::JsonObjectSubscript<const char*>')
The solution is to remove the ambiguity with any of the following replacement:
ssid = (String)network["ssid"];
ssid = (const char*)network["ssid"];
ssid = network["ssid"].asString();
ssid = network["ssid"].as<const char*>();
ssid = network["ssid"].as<String>();