JsonX: Lightweight JSON to C struct mapping for Embedded Systems

JsonX: Lightweight JSON to C struct mapping for Embedded Systems

posted 4 min read

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

    • Define once → parse many.

    • Shared parsing engine for all schemas.

  • 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
If you read this far, tweet to the author to show them you care. Tweet a Thanks

1 Comment

2 votes
1

More Posts

How to serialize JSON API requests using Java Records?

Faisal khatri - Nov 9, 2024

The Complete JSON & Data Toolkit

Anil55195570 - Sep 30

Top JSON Formatting & Validation Tools (Fast + Developer-Friendly)

Sunny - Jul 14

Working With JSON File in Python

Abdul Daim - Mar 18, 2024

Json.decoder.jsondecodeerror: expecting value: line 1 column 1 (char 0)

Muhammad Sameer Khan - Oct 19, 2023
chevron_left