JSON parsing simplified.
- Simple to use.
- No global error handling state.
- High level api jq-like for selecting properties from the parsed JSON.
- Fast. Uses SIMD 2 (for skipping whitespace).
- Differentiate between
intsandfloats - Arena allocator - freeing is O(1)
Easy JSON is a lightweight and fast JSON parser with an emphasis on ease of use
for parsing and accessing properties in the parsed JSON. I was wanting something
that had a small surface area, solid error handling and a jq like syntax for
simplifying accessing properties off JSON.
The memory is allocated using an arena, including strings, this means if you
need a string to exist past the lifetime of the struct you will need to call
something like strdup. The arena minimises the number of allocations needed
to create the json struct and simplifies freeing the memory at the cost
of almost certainly over allocating memory. Calling jsonToString or
jsonGetStrerror does return a string allocated by malloc thus needs
to be freed.
The is error handling and reporting suitable for use with multiple threads as The error state is unique per parsed JSON. Thus if you are parsing mulitple json's in parallel you can pinpoint which instance failed. The offset is saved where the parsed failed providing the character and it's offset in the JSON string.
A speed increase has been obtained by some experimentation with the 'Streaming
SIMD Extensions 2' (__SSE2__). With the usage of SIMD for skipping
whitespace it is in some instances faster than cJSON especially when used on
large JSON files. On small JSON files the speed is much the same.
You need json.c & json.h and you're good.
/* Get JSON */
json *J = jsonParse(json_string);
/* Do some things with the JSON */
if (!jsonOk(J)) {
/* Handle error */
}
/* Free up JSON, can handle nulls */
jsonRelease(J);The following functions are avalible for parsing a JSON string:
json *jsonParse(char *raw_json);
json *jsonParseWithFlags(char *raw_json, int flags);
json *jsonParseWithLen(char *raw_json, size_t buflen);
json *jsonParseWithLenAndFlags(char *raw_json, size_t buflen, int flags);JSON_STRNUM_FLAGis avalible for parsing both floats and integers as strings.
The following will return 1 if the json * is not NULL and there is a match
on the type:
int jsonIsObject(json *j);
int jsonIsArray(json *j);
int jsonIsNull(json *j);
int jsonIsBool(json *j);
int jsonIsString(json *j);
int jsonIsInt(json *j);
int jsonIsFloat(json *j);And the following for getting a value from a json *:
json *jsonGetObject(json *J);
json *jsonGetArray(json *J);
void *jsonGetNull(json *J);
int jsonGetBool(json *J);
char *jsonGetString(json *J);
ssize_t jsonGetInt(json *J);
double jsonGetFloat(json *J);A legitimate JSON NULL is parsed to JSON_SENTINAL not c's NULL. You can use
the helper jsonIsNull() to determine this.
This allows for jq like expressions to select properties from json, this makes
accessing properties on the json object much easier. It is heavily inspired by
antirez issue in cJSON with
adaptations to make it suitable for use with this parser. It is provided as a
separate file in json-selector.c so if you don't require it you are not
forced to use it.
{
"foo": "bar",
"num": 69420,
"array": [{
"id": 1,
"name": "James"
}]
}Then parse as follows:
/* Get JSON */
json *J = jsonParse(json_string);
/* Access properties on the json */
json *name = jsonSelect(J, ".array[0].name:s");
json *id = jsonSelect(J, ".array[0].id:i");
json *num = jsonSelect(J, ".num:f");
printf("name: %s\n", name->str);
printf("id: %ld\n", id->integer);
printf("num: %ld\n", num->floating);
/* Free up JSON */
jsonRelease(J);Allows selections like:
json *J = jsonParse(myjson_string);
json *width = jsonSelect(J,".foo.bar[*].baz",4);
json *height = jsonSelect(J,".tv.type[4].*","name");
json *price = jsonSelect(J,".clothes.shirt[4].price_*", price_type
== EUR ? "eur" : "usd");Do not free the returned value, only ever free the whole json structure with
jsonRelease.
You can also include a : specifier, typically at the end, to verify the
type of the final JSON object selected. If the type doesn't match, the
function will return NULL. For instance, the specifier .foo.bar:s will
return NULL unless the root object has a 'foo' field, which is an object
with a 'bar' field that contains a string. Here is a comprehensive list of
selectors:
".field" selects the "field" of the current object.
"[1234]" selects the specified index of the current array.
":<type>" checks if the currently selected type is of the specified type,
where the type can be a single letter representing:
- "s" -> string
- "f" -> float
- "i" -> int
- "a" -> array
- "o" -> object
- "b" -> boolean
- "!" -> null
In order to see where an error occured along with a human readible message can be obtained with the following code.
json *J = jsonParse(json_string);
/* Check to see if the parse worked correctly */
if (!jsonOk(J)) {
/* Will print the error to stderr */
jsonPrintError(J);
/* Get the string for your own purposes, though the method is slow
* so the checking the J->state->error would be the faster option.
*/
char *str_error = jsonGetStrerror(J);
/**
* Get the error code
*/
int error_code = jsonGetError(J);
/* Do something with the error */
free(str_error);
}
/* Free up JSON */
jsonRelease(J);main.c contains a simple program that will read in a file, parse the json, print it and time the jsonParse and jsonRelease functions.
Compile with the flag -DERROR_REPORTING if you want to print errors to
stderr. There's a debugging function that can be useful for exploring the code.
I tried a few ideas and have split out the more tricky functions into parse-string or parse-number. Which allow for seeing how they work without the clutter of the other JSON parsing. char-bitest was an expriement creating bitmaps to check for characters, which is very slow in comparison to an if (ch == ' '.
The main structure is very simple and follows the pattern of a linked list.
The conecptual difference between an array and an object in this structure is that an object has a key and an array doesn't.
typedef struct json {
jsonState *state;
json *next;
char *key;
JSON_DATA_TYPE type;
union {
json *array;
json *object;
char *str;
int boolean;
char *strnum;
double floating;
ssize_t integer;
};
} json;- Find first character, must be a
{or a[ - Either call
jsonParseObjectorjsonParseArray - Find the first character which to be valid must be one of
"{[tfn-0..9 - Based on that character set the
jsonParsertype - Call
jsonParseValuethis will pick the correct function to call and set the value on theunion - Repeat until either out of characters or there is an error which is set on
jsonParser->errno
The implementations of the parsers in order of least complexity:
jsonParseBooljsonParseNulljsonParseArray- basically moves past whitespace and callsjsonParseValuejsonParseObject- same as array but callsjsonParseStringto set thekeyjsonParseNumber- with a mantissa limit of 18 find both parts of the number as 2 ints and glue it back togetherjsonParseString- happy path is simple enough, but parsingutf-16makes the code far more tricky.
Very limited support for simd, currently to __SSE2__ which was avalible on
my macbook pro. It's used for jumping passed whitespace characters 16 characters
at a time as opposed to one by one.
- Allows duplicate keys, though so does
cJSON. - It would be fun to implement the
jsonstruct as a red black tree, it would be slower to parse JSON but potentially faster to do lookups. It feels unrealistic that you just want to parse JSON usually you want to get at something within the structure and do it quickly. - Floating point precision is a bit iffy, however the aim was to not
#include <math.h>or usestrlodwhich has been achieved. - I'm sure there is more but this is the first limitation that springs to mind.