ujson is a tiny C++ library used to read a JSON format.
Where u stands for µ (micro). Original repository where the code is maintained is
here: https://github.com/bitolabs/ujson.git
Here is a brief example:
#include "ujson.h"
#include <iostream>
int main() {
ujson::Json json;
ujson::Obj obj = json.parse(
"{"
" \"foo\" : 42,"
" \"bar\" : \"baz\""
"}").as_obj();
int32_t foo = obj.get_i32("foo");
const char* bar = obj.get_str("bar");
std::cout << foo << '\n';
std::cout << bar << '\n';
return 0;
}- Requires C++17 or later. No other dependency.
- Just two files: ujson.h, ujson.cpp. The tests are in a separate ujson-test
repo, so no junk is included in the project that uses
ujson. - Compatible with JSON specifaction RFC8259 and ECMA-404.
- The API in ujson.h is simple, easy to read and self-explanatory, rarely requiring additional documentation.
- The input is always a memory buffer. To read a JSON file, the application must load
the file in the memory as a C string and provide it to
ujson. - In-place parsing allows referencing the string tokens directly from the input buffer rather allocating each such token in the heap.
- The input is in UTF-8 format. The string tokens can contain escape sequences with UTF-16 code points.
- Number values can be fetched as integers (32 or 64 bit) or as 64-bit float values.
- Optional extended syntax that is not part of JSON standard. These can
be turned ON/OFF:
//comments.- Check that member names are unique within the object and raise an error if not. When this feature is OFF, duplicates are allowed as per JSON standard.
- Allow trailing commas in arrays and objects:
{"a": [1, 2, ], "b": 42, }. - Allow no digits after the decimal point:
[0., -1., 2.e10]. - Allow integers in hex format like:
[0x1A, 0X2b]. - Allow object member names as C identifiers without quotes:
{foo: "bar"}.
- Exceptions are used to handle the errors. See error handling.
- Value validation is easy and doesn't require a JSON schema. See:
- On error,
ujsonreports the corresponding line number. This is supported for any error, even it happens after parsing, due toujsonremembers the line number for any loaded value.
The application must provide the JSON content as a memory buffer
of char elements in UTF-8 format. Usually the JSON data resides
in a file, so the application must load the whole content into a buffer.
The application can use various APIs to read from file, there ujson
doesn't have a function to do that as to not impose a particular API.
The input buffer must be zero-terminated in case in-place parsing will be used. Otherwise the application can provide the length of the buffer.
Here is an example using C standard library:
char* str_read_file(const char* fname)
{
char* str = NULL;
FILE* f = fopen(fname, "rb");
if (NULL == f) return NULL;
fseek(f, 0, SEEK_END);
size_t size = ftell(f);
fseek(f, 0, SEEK_SET);
str = (char*)malloc(size + 1);
if (NULL == str) goto cleanup;
size_t n = fread(str, 1, size, f);
str[size] = 0;
if (n != size) {
free(str);
str = NULL;
}
cleanup:
fclose(f);
return str;
}
int main()
{
char* in = str_read_file("my.json");
...
free(in);
return 0;
}Next we have to create a variable of Json type.
This variable must be allocated until we finished with
getting all the JSON values. We must not use any values
obtained from ujson after we deallocate the Json variable.
Then we call Json::parse() or Json::parse_in_place(), see in-place parsing.
Note, in case of Json::parse() we can optionally provide
the buffer length, so that it doesn't have to be zero-terminated.
Either function returns a Val by value, which is the root value.
Val is a small, cheap-to-copy view into the parsed data (it holds
a single pointer internally); it represents a JSON value of any type.
In majority of cases the a JSON's root value is an object.
So we can obtain it as an Obj view by calling Val::as_obj()
method. Should this value be of another type, as_obj()
will raise ErrBadType exception. Check other Val::as_*()
methods for different types.
If we use the parse() method, the input buffer can be freed
by the application immediately after the call. If we use
parse_in_place(), we must keep the input buffer allocated
until we are done fetching JSON values.
int main()
{
char* in = str_read_file("my.json");
ujson::Json json;
ujson::Obj root = json.parse(in).as_obj();
...
free(in);
return 0;
}We can read values via following classes derived from Val class:
Obj: contains named values of any type. Use the get methods:get_xxx(name): where xxx is a value type such asi32, etc.get_member(name): returns aValview that can be converted to a particular type via itsas_xxx()methods.
Arr: contains an array of values of any type. Use get methods:get_xxx(idx): where xxx is a value type such asi32, etc.get_element(idx): returns aValview that can be converted to a particular type via itsas_xxx()methods.
Str: use itsget()method to get the pointer to a C string.F64: use itsget()methods to get a floating point value of adoubletype. Can be used on any numbers, integers or floating point.Int: use itsget()methods to get a int64_t value, orget_i32()to restrict values to 32-bit. Can be used only with integers.Bool: use itsget()method to obtain a value of abooltype.
A null value can be determined by verifying if Val::get_type() returns vtNull.
This is different from a missing/absent value (vtNone), covered in
Optional and default values.
Note that Val and its derived classes are lightweight views, valid only as
long as the Json instance is allocated. See more in value life time section.
Below is an example of fetching JSON values:
{
"name" : "Main Window",
"width" : 640,
"height": 480,
"on_top": false,
"opacity": 0.9,
"menu" : ["Open", "Save", "Exit"],
}int main()
{
char* in = str_read_file("my.json");
ujson::Json json;
ujson::Obj root = json.parse(in).as_obj();
std::string name = root.get_str ("name");
int32_t width = root.get_i32 ("width", 100, 4000); // restrict to range (100,4000)
int32_t height = root.get_i32 ("height", 100, 4000);
bool on_top = root.get_bool("on_top", false); // default: false
double opacity = root.get_f64 ("opacity", 0.0, 1.0, 1.0); // range (0,1), default: 1
ujson::Arr menu = root.get_arr("menu");
for (size_t i = 0; i < menu.get_len(); i++) {
std::string item = menu.get_str(i);
...
}
...
free(in);
return 0;
}Val and its derived classes (Bool, Int, F64, Str, Arr, Obj) are
small, cheap-to-copy views into data owned by the Json instance that
produced them - similar in spirit to std::string_view. They don't own or
extend the lifetime of anything; they are provided for example by:
Json::parse()andJson::parse_in_place()Arr::get_element()Obj::get_member()andObj::get_member_opt()- etc.
The library also provides plain C pointers/strings whose validity follows the same rules:
- C strings of
Strvalues. Provided for example by:Str::get()Arr::get_str()Obj::get_str()
- C string names of values. Provided for example by:
Val::get_name()Obj::get_member_name()
All of the above remain valid as long as the Json instance is allocated,
and become invalid when any of the below occurs:
Jsoninstance is deallocated.Json::parse()orparse_in_place()is called again.Json::clear()is called.
If the in-place parsing is used, then strings and names are valid until
the application deallocates the input buffer, even if the Json instance is
no longer allocated. However Val/Arr/Obj/etc. views are bound only to
the Json instance, regardless of which parsing method was used.
Error handling is done through exceptions:
Erris the baseujsonexception. It holds the correspondinglinenumber in JSON text. Even if the error occurs after parsing, eachValinstance remembers the corresponding line number where it was specified. The application can also callVal::get_line()to obtain it.ErrSyntaxis the exception that can occur duringJson::parse*()call. It indicates that JSON text is malformed.ErrValueis the exception that can occur after parsing, while the application is fetching the parsed values. It indicates that the value doesn't pass the validation imposed by the application (bad type, bad number range, etc). This exception is split in sub-classes, each indicating a special type of validation.ErrValuecontains following common attributes (sub-exceptions may contain other specific attributes):val_name- name of the failed value. Empty if value is not an object member.val_idx- index in the array or object. -1 if this is a root value.val_type- the actual type of the value.
The Err::what() method return a short message. To get more details
that includes the line number and other attributes, call Err::get_err_str().
When fetching number values, the application can specify a range.
If the value is not in range, then ErrBadIntRange or ErrBadF64Range
exception is thrown.
Example of methods performing range checking:
Int::get(lo, hi)Int::get_i32()Int::get_i32(lo, hi)F64::get(lo, hi)Arr::get_i32(idx, lo, hi)Arr::get_i64(idx, lo, hi)Arr::get_f64(idx, lo, hi)Obj::get_i32(name, lo, hi)Obj::get_i64(name, lo, hi)Obj::get_f64(name, lo, hi)
If (lo > hi), then the range is ignored.
The get_i32() methods implicitly check that the number fits in
32-bit integer range.
Str values can be restricted to a set that in the application
corresponds to an enum. If the value is not in the set, ErrBadEnum
is thrown. Example of enum methods:
Str::get_enum_idx(str_set, len)Str::get_enum(str_set, val_set)Obj::get_str_enum_idx(name, str_set, len, required=true)Obj::get_str_enum(name, str_set, val_set[, def])
Example:
enum Color { red, green, blue };
ujson::Json json;
ujson::Obj obj = json.parse(R"({"foo": "green"})").as_obj();
Color color = obj.get_str_enum("foo",
std::array{"red", "green", "blue"},
std::array{red, green, blue});For scalar members (Bool, Int, F64, Str), supplying a default value
suppresses ErrMemberNotFound for that member, as shown below.
A member that's missing entirely is different from one whose value is JSON
null: ujson represents "missing" as an absent Val — one with no
underlying data at all. Val::get_type() returns vtNone for it, and it
tests as false in a boolean context (via has_value() or
explicit operator bool()). The get_member_opt, get_arr_opt and
get_obj_opt methods are the ones that can return such a value, instead of
throwing, when the requested name isn't found.
ujson::Obj obj = ...
// By default named members are required:
//
int32_t n = obj.get_i32("n"); // will fail if "n" is absent
std::string s = obj.get_str("s"); // will fail if "s" is absent
ujson::Obj x = obj.get_obj("x"); // will fail if "x" is absent
// Handling an optional number:
//
int32_t n = obj.get_i32("n", 0, 100, 42); // will return 42 if "n" is absent
// note that (0, 100) is the range
// Handling an optional string:
//
std::string s = obj.get_str("s", "default"); // will return "default" if "s" is absent
// Handling an optional object:
//
if (ujson::Obj x = obj.get_obj_opt("x")) {
std::string child = x.get_str("child");
}
// Handling an optional value of any/unknown type: get_member_opt() never
// throws for a missing name; check it with has_value() (or just as a
// boolean condition), then convert to the expected type explicitly:
//
if (ujson::Val v = obj.get_member_opt("x")) {
ujson::Obj x = v.as_obj(); // throws ErrBadType if "x" isn't actually an object
}By default an array Arr value can have any number of elements.
However, if the application must limit the array length and reject
it if it is not valid, then it can use the Arr::require_len() method
as follows:
ujson::Json json;
// Limit the 'rgb' array to 3 elements, otherwise it will throw ErrBadArrLen.
ujson::Arr rgb = json.parse("[0, 255, 0]").as_arr().require_len(3);
...
// Bound the 'samples' length to range (0, 16), otherwise it will throw ErrBadArrLen.
ujson::Arr samples = json.parse("[0, 1, 2, 7]").as_arr().require_len(0, 16);
...
// The 'unlimted' array has no length limitation.
ujson::Arr unlimited = json.parse("[0, 1, 2, 4, 5]").as_arr();
...ujson can throw the ErrUnknownMember in case the application
doesn't recognize the name of the value. This is a good practice
of error handling, otherwise the JSON file may contain typos that
will be silently ignored, leading to unexpected application behavior.
This is how it should be handled:
- Each time the application interrogates a value (by name, or by index), it is marked by ujson as 'used'. Meaning that the application expects such a value.
- When the application finishes fetching all the values, it calls
Val::reject_unknown_members()on the root object. This will check recursively if there's any named value that is left 'unused'. If so, it will raise the exception. - For this to work correctly, the application must interrogate every known value. Normally this is what the application does, otherwise it may indicate that it never reads some values. However there may be conditions when the application ignores some values, for example depending on the content of the other values.
- If the application needs to ignore a branch of sub-values,
it can call the
Val::ignore_memberson any of the parent value of this branch. This will mark all the children as 'used'. - Note that, even if the parent value is an
Arrmeaning that it contains un-named values which are not checked byreject_unknown_members(), these array elements could contain inside some objects with named values.
When calling Json::parse_in_place(), the application provides a zero-terminated
input buffer that is modified by the library so that the string tokens are made
zero-terminated.
For example, the following input
{"foo": "bar"}\0
will be modified like:
{"foo\0: "bar\0}\0
This allows the library to provide pointers to "foo" and "bar" strings instead of allocating and copying each string in the heap.
When calling Json::parse(), the application provides a constant buffer which
doesn't need to be zero-terminated if the length is provided. In this case
the library will allocate an internal zero-terminated copy for the whole input
and will call Json::parse_in_place().
It is possible to specify in strings escape sequence with UTF-16 code-points. For example:
const char str[] =
R"(
{
"foo" : "\u00B5", // µ (MICRO SIGN) U+00B5
"bar" : "\uD83D\uDE02", // (FACE WITH TEARS OF JOY) U+1F602
}
)";The second example has two UTF-16 code points (high surrogate and low surrogate) that form one emoji character FACE WITH TEARS OF JOY.
Unit tests are in a separate repo: ujson-test.