Goal: Parse valid JSON containing only primitive values.
Supported:
null
true
false
numbers
strings
Not supported yet:
arrays []
objects {}
tinyjson/
βββ CMakeLists.txt
βββ Makefile
βββ build.sh
βββ include/
β βββ tinyjson/
βββ src/
βββ tests/
Tasks:
- Set up CMake
- Set up Makefile
- Set up build script
- Set up test framework/simple test runner
- Establish
include/andsrc/structure
Understand and implement the basic JSON scanner.
Example:
const char *json = "{\"temperature\": 28.5}";
const char *current = json;Tasks:
- Walk through characters
- Detect
'\0' - Advance pointer
- Read current character
- Skip whitespace
You'll have:
skip_whitespace()Turn characters into tokens.
Your tokenizer.h will contain:
TokenType
Token
tokenizer_next()Tasks:
-
{ -
} -
[ -
] -
: -
, - strings
- numbers
-
true -
false -
null - EOF
- error
For v1, arrays and objects can be recognized as tokens but the parser does not need to support them yet.
Example:
"hello"
β
TOKEN_STRING
123.45
β
TOKEN_NUMBER
true
β
TOKEN_TRUE
null
β
TOKEN_NULL
Implement JSON string handling.
Examples:
"hello""hello world"Eventually:
"hello\nworld"Tasks:
- Find opening
" - Find closing
" - Calculate string length
- Handle escaped quotes
- Handle basic escape sequences
- Detect unterminated strings
Support JSON numbers.
Examples:
0
10
-10
3.14
-42.5
1e10
-2.5e-3
Tasks:
- Positive integers
- Negative numbers
- Decimal numbers
- Exponents
- Reject invalid numbers
Eventually the tokenizer should produce:
Token token;with:
token.type == TOKEN_NUMBERand the parser can convert the token to:
doubleImplement:
true
false
null
For example:
true
β
TOKEN_TRUE
false
β
TOKEN_FALSE
null
β
TOKEN_NULL
Also detect invalid input:
tru
fals
nul
Create the internal representation for primitive JSON values.
Your current design is a good starting point:
typedef enum
{
JSON_NULL,
JSON_BOOL,
JSON_NUMBER,
JSON_STRING,
JSON_ARRAY,
JSON_OBJECT
} JsonType;For v1, the important types are:
JSON_NULL
JSON_BOOL
JSON_NUMBER
JSON_STRING
Then:
typedef struct JsonValue
{
JsonType type;
union
{
int boolean;
double number;
char *string;
} data;
} JsonValue;We can leave JSON_ARRAY and JSON_OBJECT in the enum as reserved for v2, or remove them from the v1 API until v2.
Now connect everything:
JSON
β
Tokenizer
β
Tokens
β
Parser
β
JsonValue
The parser should understand:
null β JsonValue(JSON_NULL)
true β JsonValue(JSON_BOOL)
false β JsonValue(JSON_BOOL)
123.4 β JsonValue(JSON_NUMBER)
"hello" β JsonValue(JSON_STRING)
Implement:
parse_value()This becomes the central function.
Conceptually:
switch (token.type)
{
case TOKEN_NULL:
// create JSON_NULL
case TOKEN_TRUE:
case TOKEN_FALSE:
// create JSON_BOOL
case TOKEN_NUMBER:
// create JSON_NUMBER
case TOKEN_STRING:
// create JSON_STRING
default:
// error
}Strings require dynamic memory.
Implement things like:
json_value_free()For example:
JsonValue
β
βββ type = JSON_STRING
β
βββ string β "hello"
When finished:
json_value_free(&value);should release anything allocated by the parser.
This becomes especially important in v2 when arrays and objects introduce much more dynamic memory.
Make invalid JSON fail cleanly.
Examples:
"hello
12.3.4
tru
nullx
You could eventually have:
typedef enum
{
JSON_SUCCESS,
JSON_ERROR_UNEXPECTED_TOKEN,
JSON_ERROR_INVALID_NUMBER,
JSON_ERROR_INVALID_STRING,
JSON_ERROR_UNEXPECTED_END
} JsonError;Build tests around each component.
whitespace
empty input
character traversal
{}
[]
:
,
true
false
null
123
"hello"
nulltrue123.45"hello"And invalid JSON:
tru
"hello
12.3.4
I'd define tinyjson v1 as:
tinyjson v1
β
ββββββββββ΄βββββββββ
β β
Tokenizer Parser
β β
ββββββββββ¬βββββββββ
β
JsonValue
β
βββββββββββββΌββββββββββββ
β β β
null bool number
β
string
It should be able to:
json_parse("null");
json_parse("true");
json_parse("false");
json_parse("123.45");
json_parse("\"hello\"");and reject malformed primitive JSON.
This is where your non-primitive types come in.
Support:
[][1, 2, 3][true, false, null]Eventually:
[1, "hello", true, null]And later nested structures:
[[1, 2], [3, 4]]Support:
{}{"name": "John"}{
"name": "John",
"age": 25
}Support:
{
"name": "John",
"scores": [10, 20, 30]
}and:
[
{"name": "John"},
{"name": "Jane"}
]This is where the recursive nature of JSON becomes important.
Handle freeing:
JsonValue
β
βββ string
β
βββ array
β βββ JsonValue
β βββ JsonValue
β βββ JsonValue
β
βββ object
βββ key β JsonValue
βββ key β JsonValue
βββ key β JsonValue
json_value_free() will need to recursively free everything.
After v2 works, you can consider:
v3
βββ Better error messages
βββ Line/column error locations
βββ UTF-8 / Unicode handling
βββ More escape sequences
βββ Pretty printing
βββ JSON serialization
βββ Streaming parser
βββ Custom allocators
βββ Performance improvements
tinyjson
β
ββββββββββββ΄βββββββββββ
β β
V1 V2
β β
Primitive JSON Compound JSON
β β
ββββββββΌβββββββ βββββββ΄ββββββ
β β β β β
null bool number arrays objects
β β β
β βββββββ¬ββββββ
string β
nested JSON