Streaming JSON emitter for bare-metal C. Single static writer, zero heap,
character-at-a-time output via a caller-supplied putc.
Probably yes if you want to emit JSON from an MCU (telemetry, config dumps, log payloads), your output sink (UART, ring buffer, file) can accept one byte at a time, and you're willing to trade features for size.
Probably no if you need to parse JSON, emit from multiple writers concurrently, or want the library to sanitize untrusted string data.
- Zero heap. State is one file-scope static struct. Size scales with
JCON_MAX_DEPTH. - Sink-agnostic. You supply
int putc(char). The library never owns a buffer. - Stateful sinks via
ctx.jcon_starttakes avoid *ctxthat the library threads into everyputccall. Use it to point at a UART handle, a ring buffer, or whatever your sink needs — no globals required. PassNULLif you don't need it. - No string escaping. Strings are emitted verbatim between
"..."; caller is responsible for ensuring the contents are valid inside a JSON string body. If you need escaping, do it before calling. This keeps the core small and avoids pulling in a UTF-8/escape ruleset you might not want. - Sticky errors. The first
putcfailure latches; subsequent emits no-op. Check once atjcon_end()— no per-call branching in caller code. - Root is an object by default. Use
jcon_start_arraywhen you need an array at the top level (handy for streaming NDJSON-style arrays of records). - Not thread-safe, not reentrant. One global writer; don't call
jcon_*from insideputc. - Typed emitters with optional
_Genericdispatch. Calljcon_add_int/jcon_add_string/ etc. explicitly, or usejcon_add(name, value)to select the right one at compile time. - Misuse asserts in debug, latches in release. Mismatched start/end,
depth overflow, and NULL keys trip
assert()under debug; withNDEBUGthey silently latchJCON_ERR_USAGE.
- C11 or later (
_Generic). snprintf,assert, and the standard fixed-width int headers.- No other dependencies.
| Macro | Default | Effect |
|---|---|---|
JCON_MAX_DEPTH |
16 |
Max nesting depth. |
JCON_INDENT |
" " |
Indent unit for pretty-print mode. |
JCON_ENABLE_FLOAT |
off | Enables jcon_add_float / jcon_add_double. |
NDEBUG |
standard | Turns misuse asserts into silent JCON_ERR_USAGE. |
static int uart_putc(void *ctx, char c) {
(void)ctx;
return uart_write(&c, 1) == 1 ? 0 : -1;
}
jcon_start(false, uart_putc, NULL);
jcon_add("id", 42);
jcon_array_start("log");
jcon_add(NULL, "boot");
jcon_array_end();
jcon_end();→
{
"id": 42,
"log": [
"boot"
]
}Full API in include/jcon.h; usage patterns and edge cases
in tests/test_jcon.c.
Compile src/jcon.c with your project and put include/ on the header
search path. Works in any toolchain that has a C11 compiler.
make # run debug, release (NDEBUG), and float test configurations
make lib # build/jcon.o for consumer linkingadd_subdirectory(path/to/jcon)
target_link_libraries(my_app PRIVATE jcon::jcon)CMake options: -DJCON_MAX_DEPTH=<N> (default 16), -DJCON_ENABLE_FLOAT=ON.
Add jcon to your west.yml, then in prj.conf:
CONFIG_JCON=y
The module exposes CONFIG_JCON_MAX_DEPTH and CONFIG_JCON_ENABLE_FLOAT
via Kconfig. A runnable native_sim sample lives at
samples/zephyr/hello_jcon/.
MIT. See LICENSE.