#ifndef RT_JSON_H #define RT_JSON_H /* * rtjson.h - Simple json parser for C * Copyright (C) 2026 Kevin Trogant * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "rtcore.h" #ifndef RTJ_API #define RTJ_API RTC_API #endif typedef enum { JSON_TYPE_OBJECT, JSON_TYPE_ARRAY, JSON_TYPE_STRING, JSON_TYPE_INTEGER, JSON_TYPE_FLOAT, } json_type; /* Represents a json value */ typedef struct json { /* The key, if this value is part of an object. * Empty otherwise. */ s8 key; json_type type; /* The value. */ union { s8 s; i64 i; f64 f; /* The first subobject. * Element 0 of an array, * one member of an object. */ struct json *first_child; } value; /* Next in the list of a parents children */ struct json *next; } json; /* Parses the given json text. * The returned json struct will be allocated on a. * file is only used for error output and can be set to {0} */ RTJ_API json *ParseJSON(s8 text, s8 file, arena *a); /* Returns a pointer to the objects member with the given key. * Returns NULL, if no such member exists */ RTJ_API json *GetJSONMember(json *o, s8 key); /* Returns a pointer to the arrays i-th element. * Returns NULL if no such element exists */ RTJ_API json *GetJSONArrayElement(json *a, isize i); /* Returns the length of the given json array or -1 if the * passed pointer is not an array */ RTJ_API isize GetJSONArrayLength(json *a); /* Iterates over every member of an object or array */ #define JsonForEach(_ChildVarName, _ParentPtr) \ for (json *_ChildVarName = (_ParentPtr)->value.first_child; _ChildVarName != 0; _ChildVarName = _ChildVarName->next) #endif #ifdef RT_JSON_IMPLEMENTATION #undef RT_JSON_IMPLEMENTATION #include typedef enum { NOT_A_NUMBER, DOUBLE, INT, } parse_number_result; static parse_number_result ParseNumber(s8 text, isize *_at, isize *current_line, s8 file, i64 *_i, f64 *_f) { isize at = *_at; if ((text.data[at] < '0' || text.data[at] > '9') && text.data[at] != '-') return NOT_A_NUMBER; int dot_count = 0, exp_count = 0; isize len = 0; while ((text.data[at + len] >= '0' && text.data[at + len] <= '9') || text.data[at + len] == '.' || text.data[at + len] == 'e' || text.data[at + len] == 'E' || text.data[at + len] == '+' || text.data[at + len] == '-') { ++len; if (text.data[at + len] == '.') ++dot_count; if (text.data[at + len] == 'e' || text.data[at + len] == 'E') ++exp_count; } if (dot_count > 1 || exp_count > 1) { if (file.data) printf("%*.s:%zu expected a number\n", (int)file.length, file.data, *current_line); else printf("%zu: expected a number\n", *current_line); return NOT_A_NUMBER; } if (dot_count || exp_count) { s8 n = {.data = &text.data[at], .length = len}; s8_parse_f64_result parsed = S8ParseF64(n); *_f = parsed.f; return parsed.ok ? DOUBLE : NOT_A_NUMBER; } else { s8 n = {.data = &text.data[at], .length = len}; s8_parse_i64_result parsed = S8ParseI64(n, 10); *_i = parsed.i; return parsed.ok ? INT : NOT_A_NUMBER; } } static s8 ParseString(s8 text, isize *_at, isize *current_line, s8 file, arena *a) { isize at = *_at; if (text.data[at] != '\"') { if (file.data) printf("%.*s:%zu expected string\n", (int)file.length, file.data, *current_line); else printf("%zu: expected string\n", *current_line); return (s8){0}; } ++at; isize first_char = at; s8 str = {.data = &text.data[first_char], .length = 0}; /* If the string contains control codes, we need to copy it into owned * memory to be able to modify it. */ b32 owns_memory = 0; isize str_at = 0; while (text.data[at] != '\"' && at < text.length) { if (text.data[at] == '\\') { /* Control codes. * If this string is still unowned, copy it into owned memory to be able * to modify it. */ if (!owns_memory) { isize total_length = at - first_char; isize i = at; while (text.data[i] != '\"' && i < text.length) ++total_length; if (i == text.length) { if (file.data) printf("%.*s:%zu unexpected end of file\n", (int)file.length, file.data, *current_line); else printf("%zu: unexpected end of file\n", *current_line); return (s8){0}; } u8 *mem = alloc(a, u8, total_length); if (at > first_char) memcpy(mem, str.data, at - first_char); str.data = mem; owns_memory = 1; } ++at; if (at == text.length) { if (file.data) printf("%.*s:%zu unexpected end of file\n", (int)file.length, file.data, *current_line); else printf("%zu: unexpected end of file\n", *current_line); return (s8){0}; } switch (text.data[at]) { case '\"': case '\\': str.data[str_at] = text.data[at]; ++str_at; ++at; break; case 'b': str.data[str_at] = '\b'; ++str_at; ++at; break; case 'f': str.data[str_at] = '\f'; ++str_at; ++at; break; case 'n': str.data[str_at] = '\n'; ++str_at; ++at; break; case 'r': str.data[str_at] = '\r'; ++str_at; ++at; break; case 't': str.data[str_at] = '\t'; ++str_at; ++at; break; case 'u': /* 4 hex digits unicode codepoint * NOT IMPLEMENTED YET. */ default: if (file.data) printf("%.*s:%zu unsupported control code '%c'\n", (int)file.length, file.data, *current_line, text.data[at]); else printf("%zu: unsupported control code '%c'\n", *current_line, text.data[at]); return (s8){0}; } } else { /* Normal character */ if (owns_memory) str.data[str_at] = text.data[at]; ++str_at; ++at; } } /* Consume terminating quotes */ ++at; str.length = str_at; *_at = at; return str; } /* Return 1 if it reaches the ned of the file */ static b32 ConsumeWhitespace(s8 text, isize *_at, isize *current_line) { isize at = *_at; while (at < text.length) { if (text.data[at] == ' ' || text.data[at] == '\t' || text.data[at] == '\r') { ++at; } else if (text.data[at] == '\n') { ++at; ++*current_line; } else { break; } } *_at = at; return at == text.length; } #define ConsumeWhitespaceNoEof(_Text, _PtrAt, _CurLine, _File, _ErrRet) \ if (ConsumeWhitespace((_Text), (_PtrAt), (_CurLine))) \ { \ if ((_File).data) \ printf("%.*s:%zu unexpected end of file\n", (int)(_File).length, (_File).data, *(_CurLine)); \ else \ printf("%zu: unexpected end of file\n", *(_CurLine)); \ return (_ErrRet); \ } #define ExpectCharacter(_Char, _Text, _At, _CurLine, _File, _ErrRet) \ if ((_Text).data[(_At)] != (_Char)) \ { \ if ((_File).data) \ printf("%.*s:%zu expected '%c', got '%c'\n", \ (int)(_File).length, \ (_File).data, \ *(_CurLine), \ (_Char), \ (_Text).data[(_At)]); \ else \ printf("%zu: expected '%c' got '%c'\n", *(_CurLine), (_Char), (_Text).data[(_At)]); \ return (_ErrRet); \ } \ (_At) += 1; static json * ParseJSONImpl(s8 text, isize *_at, json *sibling, isize *current_line, s8 file, arena *a) { isize at = *_at; json *j = alloc(a, json); j->next = sibling; if (text.data[at] == '{') { /* Parse object */ ++at; j->type = JSON_TYPE_OBJECT; ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); while (text.data[at] != '}' && at < text.length) { /* "string" : value [,] */ s8 key = ParseString(text, &at, current_line, file, a); if (!key.data) return 0; ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); ExpectCharacter(':', text, at, current_line, file, 0); ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); /* Value */ if (text.data[at] == '{' || text.data[at] == '[') { /* Object or array, recurse into it. * JSON objects are unordered, so it does not matter that we * effectively reverse the order here */ j->value.first_child = ParseJSONImpl(text, &at, j->value.first_child, current_line, file, a); if (!j->value.first_child) return 0; j->value.first_child->key = key; } else if (text.data[at] == '\"') { json *child = alloc(a, json); child->key = key; child->type = JSON_TYPE_STRING; child->value.s = ParseString(text, &at, current_line, file, a); child->next = j->value.first_child; j->value.first_child = child; } else if ((text.data[at] >= '0' && text.data[at] <= '9') || text.data[at] == '-') { /* Number. */ f64 f; i64 i; parse_number_result res = ParseNumber(text, &at, current_line, file, &i, &f); if (res == NOT_A_NUMBER) return 0; json *child = alloc(a, json); child->key = key; if (res == INT) { child->type = JSON_TYPE_INTEGER; child->value.i = i; } else /* DOUBLE */ { child->type = JSON_TYPE_FLOAT; child->value.f = f; } child->next = j->value.first_child; j->value.first_child = child; } ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); /* Consume ',' We deviate from the spec: We actually support trailing commas, * because a) thats simpler code and b) not allowing trailing commas is stupid. */ if (text.data[at] == ',') { ++at; ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); } else if (text.data[at] != '}') { if (file.data) printf("%.*s:%zu unexpected character '%c' after value\n", (int)file.length, file.data, *current_line, text.data[at]); else printf("%zu: unexpected character '%c' after value\n", *current_line, text.data[at]); return 0; } } ++at; } else if (text.data[at] == '[') { /* Parse array */ ++at; j->type = JSON_TYPE_ARRAY; json *last_child = NULL; while (text.data[at] != ']' && at < text.length) { /* value [,] ... */ ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); if (text.data[at] == '{' || text.data[at] == '[') { /* Object or array, recurse into it. */ json *child = ParseJSONImpl(text, &at, NULL, current_line, file, a); if (!child) return 0; if (!j->value.first_child) j->value.first_child = child; if (last_child) last_child->next = child; last_child = child; } else if (text.data[at] == '\"') { json *child = alloc(a, json); child->value.s = ParseString(text, &at, current_line, file, a); child->type = JSON_TYPE_STRING; if (!j->value.first_child) j->value.first_child = child; if (last_child) last_child->next = child; last_child = child; } else if ((text.data[at] >= '0' && text.data[at] <= '9') || text.data[at] == '-') { /* Number. */ f64 f; i64 i; parse_number_result res = ParseNumber(text, &at, current_line, file, &i, &f); if (res == NOT_A_NUMBER) return 0; json *child = alloc(a, json); if (res == INT) { child->value.i = i; child->type = JSON_TYPE_INTEGER; } else /* DOUBLE */ { child->value.f = f; child->type = JSON_TYPE_FLOAT; } if (!j->value.first_child) j->value.first_child = child; if (last_child) last_child->next = child; last_child = child; } ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); /* Consume ',' We deviate from the spec: We actually support trailing commas, * because a) thats simpler code and b) not allowing trailing commas is stupid. */ if (text.data[at] == ',') { ++at; ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); } else if (text.data[at] != ']') { if (file.data) printf("%.*s:%zu unexpected character '%c' after value\n", (int)file.length, file.data, *current_line, text.data[at]); else printf("%zu: unexpected character '%c' after value\n", *current_line, text.data[at]); return 0; } } ++at; } else { if (file.data) printf("%.*s:%zu unexpected character %c\n", (int)file.length, file.data, *current_line, text.data[0]); else printf("%zu: unexpected character %c\n", *current_line, text.data[0]); } *_at = at; return j; } RTJ_API json * ParseJSON(s8 text, s8 file, arena *a) { isize line = 1, at = 0; return ParseJSONImpl(text, &at, NULL, &line, file, a); } RTJ_API json * GetJSONMember(json *o, s8 key) { if (o->type != JSON_TYPE_OBJECT) return NULL; JsonForEach(child, o) { if (S8Equals(child->key, key)) return child; } return NULL; } RTJ_API json * GetJSONArrayElement(json *a, isize i) { if (a->type != JSON_TYPE_ARRAY) return NULL; isize at = 0; JsonForEach(child, a) { if (at == i) return child; ++at; } return NULL; } RTJ_API isize GetJSONArrayLength(json *a) { if (a->type != JSON_TYPE_ARRAY) return -1; isize at = 0; JsonForEach(child, a) { ++at; } return at; } #endif