Simple JSON parser written in C99
- Simple, small and easy to use, integration.
- Writing in C99: simple, small, portability.
- Robust error handling, no pointer-as-object.
- Single responsibility: when work with json in C, we only need parse the json string to C value. Json stringify is easy to implement in C.
- No memory allocations, linear memory layout, cache friendly.
- Well-tested with some real-world examples.
- Visual Studio Natvis.
- String as UTF8.
- No scientific number
- Do not use state machine for parsing
- Not the best, the fastest json parser
- Parsing require a preallocated large buffer (about kilobytes, based on file size, cannot detect buffer size requirement), buffer not an allocator (I dont often store the json value as persitent, just temp for parsing levels, game data)
- Use
longjmp
to handling error, jump from error point to parse function call, which is have disadvantages:longjmp
may works different over platformslongjmp
depends on stdlib, not work on freestanding platformslongjmp
make call stack unpredictable, and must be unwind the call stack (which are overhead, and crash prone)
After sometimes use this, I found that API have flaws of its own:
- Too verbose, not just for good reasons
- Too many params for function call, which can be better to combine as a struct
- Allocator buffer, but cannot custom allocations
- It's claimed cache-friendly, but the layout of data in memory after parsing usually un-ordered, un-lineared in hierarchy point-of-view (address of items of array come before the array itself)
[Items of a array] -> [Maybe items of other array] -> [Maybe other array JSON] -> [Array JSON] -> [... and some unpredictable value, and string/object come to the party]
- Continue to the above, reorder should help? Not exactly, how to reorder, how much reorder overhead, that too much works for simple parsing simple data format like JSON.
- No big projects usage.
- Parsing still too complex
Conclusion:
- Decisioning to exploring some best practices from other parser, MetaDesk is good example. Linked list in one single arena of memory is good enough.
- For production code, we should use battle-tested library. Mai suggestion is yyjson
- Start new project call sjson (Simplified Json, but also mean Simplified Json Parser, more simple implementation)
Belove code from Json_TokenTest.c:
#include "Json.h"
#include "JsonUtils.h" // for JsonPrint
int main(int argc, char* argv[])
{
signal(SIGINT, _sighandler);
printf("JSON token testing prompt\n");
printf("Type '.exit' to exit\n");
char input[1024];
int allocatorCapacity = 1024 * 1024; // 1MB temp buffer
void* allocatorBuffer = malloc(allocatorCapacity);
while (1)
{
if (setjmp(jmpenv) == 0)
{
printf("> ");
fgets(input, sizeof(input), stdin);
const char* json = strtrim_fast(input);
if (strcmp(json, ".exit") == 0)
{
break;
}
else
{
Json* value;
JsonError error = JsonParse(json, strlen(json), JsonFlags_NoStrictTopLevel, allocatorBuffer, allocatorCapacity, &value);
if (error.code != JsonError_None)
{
value = NULL;
printf("[ERROR]: %s\n", error.message);
}
else
{
JsonPrint(value, stdout); printf("\n");
}
}
}
}
free(allocatorBuffer);
return 0;
}
enum JsonType
{
JsonType_Null,
JsonType_Array,
JsonType_Object,
JsonType_Number,
JsonType_String,
JsonType_Boolean,
};
typedef enum JsonErrorCode
{
JsonError_None,
JsonError_WrongFormat,
JsonError_UnmatchToken,
JsonError_UnknownToken,
JsonError_UnexpectedToken,
JsonError_UnsupportedToken,
JsonError_OutOfMemory,
JsonError_InvalidValue,
JsonError_InternalFatal,
} JsonErrorCode;
typedef struct JsonError
{
JsonErrorCode code;
const char* message;
} JsonError;
typedef enum JsonFlags
{
JsonFlags_None = 0,
JsonFlags_SupportComment = 1 << 0,
JsonFlags_NoStrictTopLevel = 1 << 1,
} JsonFlags;
struct Json
{
JsonType type;
int length;
union
{
double number;
bool boolean;
const char* string;
Json* array;
JsonObjectMember* object;
};
};
struct JsonObjectMember
{
const char* name;
Json value;
};
static const Json JSON_NULL = { JsonType_Null , 0 };
static const Json JSON_TRUE = { JsonType_Boolean, 0, true };
static const Json JSON_FALSE = { JsonType_Boolean, 0, false };
JSON_API JsonError JsonParse(const char* jsonCode, int32_t jsonCodeLength, JsonFlags flags, void* buffer, int32_t bufferSize, Json* result);
JSON_API bool JsonEquals(const Json a, const Json b);
JSON_API bool JsonFind(const Json parent, const char* name, Json* result);
This project is a single-header-only library, user is free to choose build system. This instructions are for development only.
# Run REPL test
make test
# Run unit tests
make unit_test
# Make single header libary (when you want to make sure Json.h/JsonEx.h is the newest version)
make lib
When I first read an article about common mistake of c libraries are do much dynamic allocations and have no custom allocators. So I create this project to learn to make good library in C.
Like I said above, I started this project mainly for learning purpose, how to design a good API for C. Have no purpose for compete with battle-tested library. When I have any ideas for better API design, I comeback and do some hacks. If you have ideas, just make a issue in this repo.
In the first version there is a custom allocator interface. But after the long run, I found the memory of Json was throw away at one, so dynamic allocators are expensive for that. Now we just given a temporary buffer to parser, and there is a linear allocator in internal. So no dynamic allocations. Note: Next version will combine both, support passing allocator around, but have API to create one-time allocator from buffer. (Better API)
It easy to write an JsonStringify version, but the real problem in C is not that simple. You need to create Json value, create Json may need memory, so we need to care about memory allocation. That headache! Fortunately, C is static type language, so we can easily convert out data structure/object to json easily base on its types. See example below:
typedef struct Entity
{
uint32_t id;
const char* name;
} Entity;
const char* JsonifyEntity(Entity entity)
{
static char buffer[256];
snprintf(buffer, sizeof(buffer),
"{\"id\":%u,\"name\":\"%s\"}", entity.id, entity.name
);
return buffer;
}
Just rename, update, change what you do not like with your code editor.
This repo use UNLICENSE but the source code you generate from make
can be your license of choice.