An easy to use C++ 17 json library designed for embedded systems with strong memory constraints.
An easy to use C++ 17 json library designed for embedded systems with strong memory constraints.
Example fo usage:
#include <nanojsoncpp/nanojsoncpp.h>
void f()
{
std::string_view json_input = R"({ "my_value": 12345, "my_string": "A string!" })";
std::array<nanojsoncpp::json_value_desc, 10u> descriptors;
auto json_val = nanojsoncpp::parse(json_input, descriptors);
if (json_val)
{
auto my_value = json_val->get("my_value");
auto my_string = json_val->get("my_string");
if (my_value && my_string)
{
int val = my_value->get<int>();
std::string_view str = my_string->get();
// Use val and str...
}
}
}
- No dynamic memory allocations : in-situ parsing of provided json string
- No recursive parsing : stack consumption is totally predictible
- No use of exceptions : remove the need of code and stack usage dedicated to exception management
- No need of Run-Time Type Information (RTTI) : allow to compile without RTTI for a smaller generated code
- No external dependencies : use only standard C++ 17 features and 1 backported C++ 20 feature (std::span)
- Heavily tested with 100% of line and functions coverage and nearly 100% on branch coverage
- Low memory footprint : less than 4kBytes on armv7-m with IAR compiler when using both parsing and generating functions
- Maximum json input string size is 65535 bytes (can be increased to 4GBytes using configuration options)
- Maximum 255 levels of imbricated objects/arrays
- Strings: only UTF-8 strings are supported without any unicode escaping (ex: \uABCD is not supported)
- Decimals:
- only fractional representation is supported, not exponent representation (ex: 3.1415926 is supported, 3.141593e+00 is not supported)
- default decimal data type is
double
(can be reduced tofloat
using configuration option)
- Integers: default integer data type is
int64_t
(can be reduced toint32_t
using configuration option)
- A fully C++17 compliant compiler
That's all :)
The configuration is done using the options described in CMakeLists_Options.txt. From this, CMake will automatically generate the corresponding nanojsoncpp_cfg.h
file in the build directory and make it available to the compiler.
The configuration is done by editing the nanojsoncpp_cfg.h provided in the config
directory. Be sure to add this path in your compiler's include path.
For a standalone build, the build is done the usual CMake way:
mkdir build
cd build
cmake ../
cmkae --build.
If used in an already existing project, just add the source directory in an upper CMakeLists.txt file:
add_subdirectory(nanocppjson)
All the sources files to compile are in the src
directory. Only the path to the inc
directory must be added in your compiler's include path.
Generation is done using the nanojsoncpp::generate_XXX()
functions. All these functions have a similar behavior :
- An input buffer is provided using an
std::span<char>
object - json string is written in the provided buffer by the
nanojsoncpp::generate_XXX()
functions - If the buffer is too small to write the json string, an empty
std::span<char>
object is returned - If the buffer is large enough, a subspan of the provided buffer pointing to the unused part of the buffer is returned so that it can be reused in further calls to
nanojsoncpp::generate_XXX()
functions
Error management is easy to implement : user application only has to check if the returned std::span<char>
object is empty or not.
Here is an example of usage of all these functions:
char tmp[200];
auto left = nanojsoncpp::generate_object_begin("", tmp);
left = nanojsoncpp::generate_object_begin("my_object", left);
left = nanojsoncpp::generate_value("value1", left, true);
left = nanojsoncpp::generate_value("value2", true, left, true);
left = nanojsoncpp::generate_value("value3", "My string!!"sv, left, true);
left = nanojsoncpp::generate_value("value4", 12345678901, left, true);
left = nanojsoncpp::generate_value("value5", 3.14, left, false);
left = nanojsoncpp::generate_object_end(left, true);
left = nanojsoncpp::generate_array_begin("my_array", left);
left = nanojsoncpp::generate_value("", left, true);
left = nanojsoncpp::generate_value("", false, left, true);
left = nanojsoncpp::generate_value("", "Another string!!"sv, left, true);
left = nanojsoncpp::generate_value("", 9876543210, left, true);
left = nanojsoncpp::generate_value("", 14.3, left, false);
left = nanojsoncpp::generate_array_end(left, false);
left = nanojsoncpp::generate_object_end(left, false);
if (!left.empty())
{
size_t json_str_size = sizeof(tmp) - left.size();
std::cout << "JSON = " << json_str_size << " : " << std::string_view(tmp, size) << std::endl;
}
else
{
std::cout << "Provided buffer too short!" << std::endl;
}
Output:
JSON = 166 : {"my_object":{"value1":null,"value2":true,"value3":"My string!!","value4":12345678901,"value5":3.14000},"my_array":[null,false,"Another string!!",9876543210,14.30000]}
Note: The nanojsoncpp::generate_XXX()
functions doesn't add a final '\0'
char to the provide input buffer.
Parsing is done using the nanojsoncpp::parse()
function. The provided input json string can be of any type which can be converted to a std::string_view
object:
- C string litterals :
const char* json_input = R"({ "my_value": 12345, "my_string": "A string!" })";
auto json_val = nanojsoncpp::parse(json_input, descriptors);
char
array :
char json_input[100u];
// json_input array is filled elsewhere
auto json_val = nanojsoncpp::parse({json_input, strlen(json_input)}, descriptors);
std::string
:
std::string json_input;
// json_input string is filled elsewhere
auto json_val = nanojsoncpp::parse(json_input, descriptors);
The json_val
variable is of type std::optional<nanojsoncpp::json_value>
.
A valid json input string will return a non empty json_val
so the success of the parse operation can simply be done by the following test :
if (json_val)
{
// Valid json string, data can be processed
}
else
{
// Invalid json string
}
The nanojsoncpp::json_value
object allow to retrieve the data associated to a json value. A json value can be of one of the following types:
enum class json_value_type : uint8_t
{
/** @brief Null value */
null,
/** @brief Boolean value */
boolean,
/** @brief String value */
string,
/** @brief Integer value */
integer,
/** @brief Integer or floating point value */
decimal,
/** @brief Array */
array,
/** @brief Object */
object
};
The nanojsoncpp::json_value
object provides convenient accessors to identify the data it handles :
/** @brief Get the type of the value */
json_value_type type() const;
/** @brief Indicate if the value is of the null type */
bool is_null() const;
/** @brief Indicate if the value is of the boolean type */
bool is_bool() const;
/** @brief Indicate if the value is of the string type */
bool is_string() const;
/** @brief Indicate if the value is of the integer type */
bool is_integer() const;
/** @brief Indicate if the value is of the decimal type */
bool is_decimal() const;
/** @brief Indicate if the value is of the array type */
bool is_array() const;
/** @brief Indicate if the value is of the object type */
bool is_object() const;
Additionaly, it allows to retrieve the name of the data using the following method:
/** @brief Get the name of the value */
std::string_view name() const;
A json value of json_value_type::null
type has no dedicated method to interact with.
To retrieve the value of a json_value_type::boolean
type, use the following method :
if (json_val.is_boolean())
{
bool value = json_val.get<bool>();
// Use value
}
Note: The actual value is computed during the call to json_val.get<bool>()
. To optimize performances when multiple accesses are needed to the json_value, store the result of the get
operation and use it instead of calling the get
operation multiple times.
A json string may contained escaped chars like \n
, \t
... nanojsoncpp allow to retrieve both the following views of the json string:
- Raw string: this is the string as it is written in the input json string
- Escaped string: a copy of the input json string where escaped chars have been remplaced by there value
To retrieve the raw value of a json_value_type::string
, use the following method :
if (json_val.is_string())
{
std::string_view value = json_val.get();
// Use value
}
To retrieve the escaped value of a json_value_type::string
, use the following method :
if (json_val.is_string())
{
char tmp[100u];
std::string_view value = json_val.get_escaped(tmp);
// Use value
}
Note 1: The returned std::string_view
object uses as its underlying buffer the tmp
variable. If the tmp
variable is not large enough to store the escaped string, the returned value will be empty. The provided buffer size must have at least the size of the raw string.
Note 2: The actual value is computed during the call to json_val.get_escaped()
. To optimize performances when multiple accesses are needed to the json_value, store the result of the get
operation and use it instead of calling the get
operation multiple times.
To retrieve the value of a json_value_type::integer
type, use the following method :
if (json_val.is_integer())
{
T value = json_val.get<T>();
// Use value
}
Where T
can be of any integral type which can be converted from int64_t
using the static_cast<>
operator (ex: int
, unsigned int
, int8_t
, ...).
Example to retrieve a value as an int16_t
:
if (json_val.is_integer())
{
int16_t value = json_val.get<int16_t>();
// Use value
}
Note: The actual value is computed during the call to json_val.get<T>()
. To optimize performances when multiple accesses are needed to the json_value, store the result of the get
operation and use it instead of calling the get
operation multiple times.
To retrieve the value of a json_value_type::decimal
type, use the following method :
if (json_val.is_decimal())
{
T value = json_val.get<T>();
// Use value
}
Where T
can be of any floating point type which can be converted from double
using the static_cast<>
operator (ex: float
...).
Example to retrieve a value as a float
:
if (json_val.is_decimal())
{
float value = json_val.get<float>();
// Use value
}
Note: The actual value is computed during the call to json_val.get<T>()
. To optimize performances when multiple accesses are needed to the json_value, store the result of the get
operation and use it instead of calling the get
operation multiple times.
The following example presents the operations to access the json values contained in a json_value_type::array
type:
if (json_val.is_array())
{
// Retrieve the number of values in the array
json_size_t count = json_val.size();
// Access the value stored at index 3 of the array
auto value_at_3 = json_val[3u];
if (value_at_3)
{
// The value exists, check its type
json_value_type type = value_at_3->type();
// Use value
}
// Alternative to access the value stored at index 3 of the array
auto value_at_3_alt = json_val.get(3u);
if (value_at_3_alt)
{
// The value exists, check its type
json_value_type type = value_at_3_alt->type();
// Use value
}
// Iterate through the whole array
for(const auto& value: json_val)
{
// Check its type
json_value_type type = value.type();
// Use value
}
}
Note 1: A json array may contain values of different types.
Note 2: The json values contained in an array have always an empty name.
The following example presents the operations to access the json values contained in a json_value_type::object
type:
if (json_val.is_object())
{
// Retrieve the number of values in the object
json_size_t count = json_val.size();
// Access the value named 'my_int' in the object
auto my_int_val = json_val["my_int"];
if (my_int_val)
{
// Use value
int my_int = my_int_val->get<int>();
}
// Alternative to access the value named 'my_int' in the object
auto my_int_val_alt = json_val.get("my_int");
if (my_int_val_alt)
{
// Use value
int my_int = my_int_val_alt->get<int>();
}
// Iterate through the whole object
for(const auto& value: json_val)
{
// Check its type
json_value_type type = value.type();
// Use value
}
}
nanocppjson does not perform dynamic memory allocation. Instead, nanocppjson uses a pool of descriptors provided by the user application to operate.
A descriptor size in memory is 10 bytes:
struct json_value_desc
{
/** @brief Nesting level of the json value */
uint8_t nesting_level;
/** @brief Type of the json value */
json_value_type type;
/** @brief Index of the begining of the name of the value in the json string */
json_size_t name_start;
/** @brief Size of the name of the value in bytes in the json string */
json_size_t name_size;
/** @brief Index of the begining of the value in the json string */
json_size_t value_start;
/** @brief Size of the value in bytes in the json string */
json_size_t value_size;
};
nanocppjson needs 1 descriptor per json data to parse (array, array value, object, object value).
The following json string would need 10 descriptors to be parsed:
{
"value1": 1234,
"value2": true,
"array1": [1.23, 2.34, 4.56],
"object1":
{
"value3": null,
"value4": "My value"
}
}
The json data structure is internally decomposed like this:
- Unamed object
- value1
- value2
- array1
- array1[0]
- array1[1]
- array1[2]
- object1
- value3
- value4
=> 10 descriptors
If the number of descriptors provided to the nanojsoncpp::parse()
is insufficient, the following error code is returned : json_error::not_enough_memory
.
An optional error callback can be provided to the nanojsoncpp::parse()
function. This callback allow to locally save the error code and the index in the json string at which the error occured.
The usual usage is like this:
std::array<nanojsoncpp::json_value_desc, 5u> descriptors;
nanojsoncpp::json_size_t error_index = 0u;
nanojsoncpp::json_parse_error error = nanojsoncpp::json_parse_error::no_error;
auto json_obj = nanojsoncpp::parse(json_input,
descriptors,
[&](nanojsoncpp::json_size_t _error_index, nanojsoncpp::json_parse_error _error)
{
error_index = _error_index;
error = _error;
});
if(json_obj)
{
// Handle json data
}
else
{
// Error handling
std::cout << "Parse error at index " << error_index " : code = " << static_cast<int>(error) << std::endl;
}
nanojsoncpp welcomes contributions. When contributing, please follow the code below.
- The .clang-format file at the root of the source tree must not be modified (or after having a discussion between all the contributors)
- The code must formatted using the above mentionned file with a clang-format compliant tools (ex: Visual Studio Code)
- Every interface/class/method must be documented using the Doxygen format
- No dynamic memory allocation is allowed
- Use of C/C++ macros is discouraged
- Keep code simple to understand and don't be afraid to add comments!
Feel free to submit issues and enhancement requests.
Please help us by providing minimal reproducible examples, because source code is easier to let other people understand what happens. For crash problems on certain platforms, please bring stack dump content with the detail of the OS, compiler, etc.
Please try breakpoint debugging first, tell us what you found, see if we can start exploring based on more information been prepared.
Follow the "fork-and-pull" Git workflow :
- Fork the repo on GitHub
- Clone the project to your own machine
- Checkout a new branch on your fork, start developing on the branch
- Test the change before commit, Make sure the changes pass all the tests, please add test case for each new feature or bug-fix if needed.
- Commit changes to your own branch
- Push your work back up to your fork
- Submit a Pull request so that we can review your changes
Be sure to merge the latest from "upstream" before making a pull request!