JsonX: Lightweight JSON → C struct mapping for Embedded Systems
JsonX is a lightweight C library for embedded developers who need to map JSON directly into structs — without dynamic memory or code generators.
JSON is everywhere - from web APIs to IoT devices.
But on microcontrollers, most JSON libraries feel like elephants in a china shop: they assume megabytes of RAM, malloc on every step, and a friendly OS.
Typical JSON libraries for embedded systems fall into two extremes:
- Full-featured giants (e.g. ArduinoJson, jansson): powerful but memory-hungry.
- Bare parsers (e.g. JSMN, cJSON, jfes): no mapping, just tokenization.
The Problem with JSON on Embedded Devices
When you work with RTOS or bare-metal firmware where every byte and cycle counts.
That’s where JsonX comes in - a lightweight layer over cJSON designed specifically for MCU + RTOS environments, where predictable memory and simplicity matter most.
So you either waste RAM or write tons of manual traversal code:
// classical pain
value = cJSON_GetObjectItem(root, "config");
param = cJSON_GetObjectItem(value, "baudrate");
baudrate = param->valueint;
JsonX in One Sentence
A thin mapping layer that turns JSON into C structs using a declarative table, not hand-written traversal.
Example structure:
typedef struct
{
char device_name[32];
double baudrate;
bool debug;
} config_t;
config_t config;
Mapping:
JX_ELEMENT config_desc[] =
{
JX_PROPERTY_STRING("device_name", config.device_name),
JX_PROPERTY_NUMBER("baudrate", config.baudrate),
JX_PROPERTY_BOOLEAN("debug", config.debug),
};
const size_t config_desc_size = sizeof(config_desc)/sizeof(config_desc[0]);
And that’s it - one call:
const char *config_json_str = "{\"device_name\":\"sensor1\",\"baudrate\":9600,\"debug\":true}";
if(jx_json_to_struct(char *buffer, config_desc, config_desc_size, JX_MODE_STRICT) == JX_SUCCESS)
{
jx_log("Configuration loaded: %s @ %lu\n", config.device_name, (uint32_t)config.baudrate);
}
No malloc() surprises, no deep tree traversal.
Core Benefits
✅ Predictable Memory
All allocations go through RTOS pools or a static buffer.
No hidden malloc/free. Works under ThreadX, FreeRTOS, or bare-metal.
✅ Automatic Struct Mapping
✅ Optional / Ignored Fields
Unknown keys ignored automatically.
Optional fields can have defaults.
Makes firmware tolerant to JSON changes.
✅ Readable and Debuggable
- You still get readable JSON in logs - easier to verify on target or
via serial output.
⚖️ JsonX vs Binary Formats
When NOT to use JsonX:
You control both ends and need max bandwidth (LoRa, NB-IoT)
You rely on schema versioning (protobuf/flatbuffers)
When JsonX shines:
You want human-readable logs
You interface with web APIs
You want deterministic memory use
You prefer C-only solution
Example: Parsing an Array of Coordinates
#define JX_USER_BUFFER_SIZE 1024
// Initialize JsonX (using standard malloc/free)
jx_init();
// Define point structure
typedef struct {
double x;
double y;
} Point_t;
// Define square structure with array of 4 points
typedef struct {
Point_t points[4];
} Square_t;
// Create square data structure
Square_t square = {
.points = {
{0, 0},
{1, 0},
{1, 1},
{0, 1}
}
};
// Define JSON mapping for a single point object
JX_ELEMENT point_0[] = {
JX_PROPERTY_NUMBER("x", square.points[0].x),
JX_PROPERTY_NUMBER("y", square.points[0].y)
};
JX_ELEMENT point_1[] = {
JX_PROPERTY_NUMBER("x", square.points[1].x),
JX_PROPERTY_NUMBER("y", square.points[1].y)
};
JX_ELEMENT point_2[] = {
JX_PROPERTY_NUMBER("x", square.points[2].x),
JX_PROPERTY_NUMBER("y", square.points[2].y)
};
JX_ELEMENT point_3[] = {
JX_PROPERTY_NUMBER("x", square.points[3].x),
JX_PROPERTY_NUMBER("y", square.points[3].y)
};
// Define array of point objects
JX_ELEMENT square_array[] = {
JX_OBJECT_VAL(point_0),
JX_OBJECT_VAL(point_1),
JX_OBJECT_VAL(point_2),
JX_OBJECT_VAL(point_3)
};
// Define root object with "square" property
JX_ELEMENT root[] = {
JX_PROPERTY_ARRAY("square", square_array)
};
// ===== SERIALIZATION: C struct -> JSON =====
char *json_output = jx_alloc_memory(JX_USER_BUFFER_SIZE);
if (json_output != NULL) {
jx_struct_to_json(root, 1, json_output, JX_USER_BUFFER_SIZE, JX_FORMATTED);
jx_log("Serialized JSON:\n%s\n\n", json_output);
jx_free_memory(json_output);
}
// ===== DESERIALIZATION: JSON -> C struct =====
const char *json_input =
"{"
" \"square\": ["
" {\"x\": 5, \"y\": 6},"
" {\"x\": 7, \"y\": 8},"
" {\"x\": 9, \"y\": 10},"
" {\"x\": 11, \"y\": 12}"
" ]"
"}";
// Parse JSON
jx_json_to_struct((char*)json_input, root, 1, JX_MODE_STRICT);
// Print parsed data
jx_log("Parsed square points:\n");
for (int i = 0; i < 4; i++) {
jx_log(" Point %d: x=%d, y=%d\n",
i,
(int)square.points[i].x,
(int)square.points[i].y);
}
// Cleanup
jx_parser_deinit();
Tested Configuration: STM32H7 + ThreadX (-Os, GCC 12)
⚙️ Memory & Code Footprint
Verified on STM32H7 (GCC 12, -Os, ThreadX)
jx_parser.o — 2228 B code, 4 B static RAM
Core parser and mapping logic.
jx_static_allocator.o — 248 B code, 4 B static RAM
Implements static allocator for bare-metal mode.
jx_version.o — 68 B code, 0 B RAM
Simple version string helper.
example.o (demo) — 765 B code, 1076 B RAM
Demo test including a 1 KB byte pool for ThreadX.
➡ Core only: ≈ 2.5 KB code + 8 B static RAM
➡ With example: ≈ 3.2 KB code + 1.06 KB RAM
The demo includes a 1 KB jx_byte_pool_buffer for ThreadX, but the actual pool size is user-defined.
JsonX itself has no hidden heap allocations - all memory management is deterministic.
The static allocator consumes only ≈ 12–16 bytes of header overhead inside the provided buffer.
Open Source and Final Thoughts
GitHub: https://github.com/embedmind/JsonX
JsonX is lightweight, open-source (MIT), and built purely in C for portability across RTOSes and MCU families.
Final Thoughts
This library isn’t about inventing a new protocol - it’s about making life easier for embedded developers who already have to deal with JSON.
Working with JSON on MCUs? I'd love to hear your use case - especially if you've battled with JSMN, Jansson, or ArduinoJson before.
Join the Discussion
- ⭐ Star on GitHub if this solves your problem
- Found a bug? Open an issue