cjson is the fastest header-only C++26 JSON library: providing SIMD structural parsing,
validation, minification, serialization, and writing, a lazy functional layer, and a native
compile-time (constexpr) mode. Every function is constexpr qualified.
Warning
cjson is built with the micron core library; there is no support for the traditional C++ Standard Library or (g)libc.
- header-only
- clean and simple API
- fully RFC 8259 compliant
- two-stage SIMD engine: 64-byte blocks to structural indexes / quote/escape masks / UTF-8 validation in one sweep, then a scalar FSM building a contiguous 16-byte value arena
- full simd support for amd64 (SSSE3/AVX2+PCLMUL), aarch64 (NEON), armv7 (NEON), and a SWAR floor
- native comptime mode:
cjson::ct::parse<S>()runs during compiletime and produces a document into two flat rodata arrays; validate, minify and serialize areconstevaltoo - on-demand reads
- minification, prettification, and serialization
- a lazy functional layer:
fmap/filter/fold/take/flat_map, curried for OCaml-style|pipes - one-shot helpers:
cjson::get<i64>(text, "/a/b") anyoverloads for simple development:cjson::get(text, "/a/b") -> micron::any<...>, carrying objects and arrays as navigable handles, not just scalars- mutate in place:
d["name"] = "new_name", add and remove members and elements, then write the document back out - a porcelain layer over micron containers:
cjson::to_map(root)into any concept-satisfyingmicronmap, and back to json again - io_uring backed native file i/o
- header-only, freestanding-capable, depends only on the micron core library
#include <cjson/cjson.hpp>
i64 port = cjson::get_or<i64>(body, "/listen/port", i64(8080));
// on-demand
cjson::scratch sc;
auto v = cjson::iterate(body, sc);
auto root = v.cast<cjson::view>().root();
i64 exp = root["exp"].i64_or(0);
auto sub = root["sub"].str_raw();
auto add = root["add"]; // yields a micron::any type
// owning document
auto r = cjson::parse(body);
if ( r.is_second() ) { /* cjson::error_name(r.cast<cjson::error>()) */ }
const cjson::doc &d = r.cast<cjson::doc>();
// a lazy pipeline over it
namespace cj = cjson;
i64 total = d.root()["users"].items()
| cj::filter_c(cj::field_bool("active"))
| cj::pluck_c("age") | cj::fmap_c(cj::to_i64)
| cj::fold_c(i64(0), cj::plus);
// build a response
cjson::builder b;
b.obj().kv("id", id).kv("name", name).end();
send(b.out().ptr, b.out().len);
// bake a config at build time
static constexpr cjson::ct::str k_cfg{ R"({"port":8080})" };
constexpr auto k_tree = cjson::ct::parse<k_cfg>();
static_assert(k_tree.root()["port"].i64_or(0) == 8080);Every function on the parse/validate/minify/write path is constexpr. Machine-specific
fast paths (SIMD kernels, __builtin_memcpy puns, abcmalloc arenas) sit behind
if !consteval with portable twins producing identical results. Comptime scratch is transient new[].
Displayed as two separate categories The current armada of json parsers is split in two:
- cjson,
simdjson-domand yyjson build a flat index over the caller's bytes. Strings are never materialized; cjson stores{offset, length}and unescapes on demand; objects get no key index, lookup is a linear scan at access time; teardown is onefree. glz::generic, rapidjson, boost.json and nlohmann build an owning, mutable tree. Every key and every string value is its ownstd::string, every array a vector that reallocs as it grows, every object a map that builds a lookup index; teardown is a recursive destructor walk.
Ranking all eight parsers in one list is dubious, as such for correctness the two categories are charted separately.
Throughput, GB/s, higher is better. AMD Ryzen 7 3700U, kernel 7.1.7, GCC 16.1.1,
taskset -c 0, medians of 7. Contender versions are recorded in the header of
benches/results/corpus_vs.txt, which is committed alongside the charts so any figure
here can be re-derived and diffed. Every contender parses the same bytes in copy mode, and every
contender's object is compiled equivalently
-O3 -march=native -flto -ffat-lto-objects.
Rows say what they amortize. cjson-reuse borrows a warm scratch and simdjson-dom(warm)
reuses its parser across reps; plain cjson, yyjson, rapidjson, glz::generic and
boost.json allocate and free per op.
glz::generic is named in full because the tier matters: it is Glaze's schema-less fallback
tree, which its own documentation notes carries an allocation cost, and not its
compile-time-reflected fast path. See the lazy chart below and benches/lazy_vs.cpp.
Reproduce with scripts/fetch_corpus && scripts/vsbuild benches/corpus_vs.cpp && taskset -c 0 ./bin/corpus_vs, and graph it with scripts/chart_corpus (--mode github
cyc/op, lower is better, log scale.
Each x position is N fields resolved from one document handle on sample/twitter.json
(632 KB, 100 records), N from 1 to 64; an index is amortized while a walk is computed N times.
At N=1 glz::lazy_json touches 204 of 631,659 bytes (0.03%) and returns, whereas cjson's iterate
reads and validates all of them.
Reproduce with scripts/vsbuild benches/lazy_vs.cpp && taskset -c 0 ./bin/lazy_vs, and
graph it with scripts/chart_vs benches/results/lazy_vs.txt --metric cyc --mode github.
All entry points live in namespace cjson. Types are micron types.
namespace cjson {
// types
struct jnull { }; // json null, distinct from "no value"
struct vref { const doc *d; const value *v; };// a dom subtree, 16 bytes, navigable
struct jraw { strv text; }; // a span of the caller's json text
using pun = micron::any<bool, u64, i64, f64, micron::string, vref, jraw, jnull>;
using bytes = micron::raw_slice<const u8>; // borrowed input view
using wbytes = micron::raw_slice<u8>; // buffer
using strv = micron::raw_slice<const char>; // strings out of getters ({.ptr,.len})
using fjson = micron::slice<u8>; // owned byte output
enum class kind : u8 { none, raw, null, boolean, number, string, array, object };
enum class error : i32 {
ok, bad_syntax, bad_number, bad_string, bad_escape, bad_utf8, bad_surrogate,
depth_exceeded, trailing_garbage, empty_input, short_output, wrong_type,
no_such_field, out_of_range, oom,
};
constexpr const char *error_name(error) noexcept;
// result<T> is micron::option<T, error>.
// success is is_first(), failure is is_second(), payload via cast<T>()
template <typename T> using result = micron::option<T, error>;
struct opts {
bool numbers_as_raw : 1 = false; // store numbers as raw {ofs,len}, no conversion
bool skip_utf8 : 1 = false; // skip utf-8 validation of the input
bool stop_when_done : 1 = false; // accept trailing bytes after the first root
bool relaxed : 1 = false; // comments + trailing commas (not implemented yet)
bool with_write_bound : 1 = false; // accumulate O(1) writer bounds during stage 2
};
struct style {
u8 indent = 0; // 0 = minified; 2/4 pretty
bool ascii_only = false; // not implemented yet
};
template <typename C> concept byte_source = /* iterable container of trivially-copyable */;
template <typename C> concept text_source = byte_source<C> || micron::is_string<C>;
bytes as_bytes(const C &); wbytes as_wbytes(C &);
constexpr strv as_strv(const char *); // + strv and has_cstr overloads
// scratch and doc
struct scratch { // move-only
constexpr bool ensure(usize) noexcept; // idx
constexpr bool ensure_pool(usize) noexcept;
constexpr bool ensure_vals(usize) noexcept;
constexpr void release() noexcept; // the three buffers release independently
};
class doc { // move-only
constexpr val root() const noexcept;
constexpr usize size() const noexcept; // value-arena slots
constexpr usize consumed() const noexcept; // byte offset one past the root
constexpr bool borrowed() const noexcept;
constexpr bool alive() const noexcept;
constexpr void release() noexcept;
// mutating in place fns
constexpr val operator[](strv|const char*|usize) const noexcept;
constexpr mut operator[](strv|const char*|usize) noexcept;
constexpr mut edit() noexcept; // mutable root, for chaining
constexpr error mut_error() const noexcept;
constexpr void clear_mut_error() noexcept;
};
class mut : public val {
const mut &operator=(bool|integral|floating|const char*|strv|is_string|nullptr) const;
constexpr error set(i64|u64|f64|bool|strv|const char*|is_string) const noexcept;
constexpr error set_null() const noexcept;
constexpr error set_number(bytes) const noexcept; // via the parser's own kernel
// objects
constexpr mut insert(strv|const char*|is_string) const noexcept; // create-or-find
constexpr mut insert_object(...) const noexcept; // create-or-find as empty {}
constexpr mut insert_array(...) const noexcept; // create-or-find as empty []
constexpr error rename(strv from, strv to) const noexcept; // in place, keeps position
constexpr error erase(strv|const char*|is_string) const noexcept;
// arrays
constexpr mut push_back() const noexcept; // appends null, hands back the slot
constexpr mut push_object() const noexcept;
constexpr mut push_array() const noexcept;
constexpr error erase(usize) const noexcept;
constexpr error clear() const noexcept; // either container -> {} or []
};
// parse / validate
// every fn below also takes: (const char*, usize), (const u8*, usize), strv, bytes, any byte container, and any micron::is_string
result<doc> parse(bytes, opts = {}); // owning
result<doc> parse(bytes, opts, scratch &); // owning, warm scratch
result<doc> parse_reuse(bytes, opts, scratch &); // borrows the scratch
result<doc> parse_insitu(wbytes, opts = {}); // rewrites the input
result<doc> parse_insitu(C &, opts = {}); // mutable container/string
result<doc> parse_insitu_reuse(wbytes, opts, scratch &);
constexpr error validate(bytes, opts = {}) noexcept;
constexpr error validate(bytes, opts, scratch &) noexcept;
constexpr bool is_valid(bytes, opts = {}) noexcept;
// general usage fns
constexpr kind type() const noexcept;
constexpr usize size() const noexcept; // val: elements / PAIRS / bytes by kind
constexpr usize count() const noexcept; // cur: same, by walking
constexpr explicit operator bool() const noexcept;
constexpr bool is_null() const noexcept;
// lossy
constexpr i64 i64_or (i64 = 0) const noexcept;
constexpr u64 u64_or (u64 = 0) const noexcept;
constexpr f64 f64_or (f64 = 0) const noexcept;
constexpr bool bool_or(bool = false) const noexcept;
constexpr strv str_or (strv = {}) const noexcept; // val: escapes decoded
constexpr strv str_raw() const noexcept; // cur: escapes retained
constexpr max_t str(wbytes out) const noexcept; // cur: decode into your buffer
constexpr result<i64> try_i64() const noexcept;
constexpr result<u64> try_u64() const noexcept;
constexpr result<f64> try_f64() const noexcept;
constexpr result<bool> try_bool() const noexcept;
constexpr result<strv> try_str() const noexcept;
// navigation
constexpr val/cur operator[](strv key) const noexcept; // first match wins on dupes
constexpr val/cur at(usize i) const noexcept; // unambiguous array index
constexpr val/cur at_pointer(strv ptr) const noexcept; // rfc 6901; "" names the root
// iteration
constexpr arr_range items() const noexcept; // yields val
constexpr obj_range members() const noexcept; // yields member { strv key; val v; }
constexpr cur_arr_range items() const noexcept; // yields cur
constexpr cur_obj_range members() const noexcept; // yields cur_member { strv key; cur v; }
// on-demand
result<view> iterate(bytes, scratch &); // opts defaulted
result<view> iterate(bytes, opts, scratch &);
class view { constexpr cur root() const noexcept; constexpr bool alive() const noexcept; };
// functional adaptors/lazy pipelines
// adaptors eager, function-first curried, range-last
fmap(fn, r) filter(p, r) fmap_c(fn) filter_c(p)
reject(p, r) take(n, r) drop(n, r) reject_c(p) take_c(n) drop_c(n)
take_while(p, r) drop_while(p, r) take_while_c(p) drop_while_c(p)
enumerate(r) flat_map(fn, r) enumerate_c() flat_map_c(fn)
keys(r) values(r) pluck(key, r) keys_c() values_c() pluck_c(key)
// terminals
fold(r, init, fn) count(r) count_if(p, r)
any_of(p, r) all_of(p, r) none_of(p, r)
find_first(p, r) -> result<T>
max_by(proj, r) min_by(proj, r) -> result<T>
for_each(fn, r)
// ... each with a _c (curried) form: fold_c(init, fn), any_of_c(p), find_first_c(p)
// terminal
collect_into<C>(r) collect_into_c<C>()
// combinators and projections
plus minus times max_of min_of
to_i64 to_u64 to_f64 to_bool to_str
is_truthy is_kind(k) has(key) key_is(key)
field(key)
field_i64(key) field_u64(key) field_f64(key) field_bool(key) // ordered/typed
// one shot helpers
template <class T> result<T> get(jtext, jptr, opts = {}); // i64/u64/f64/bool/string
template <class T> result<T> get(jtext, jptr, scratch &);
template <class T> T get_or(jtext, jptr, T dflt, opts = {});
result<strv> get_str_raw(jtext, jptr, scratch &); // borrows
bool exists(jtext, jptr, opts = {});
kind kind_at(jtext, jptr, opts = {});
result<usize> count_at(jtext, jptr, opts = {});
error each(jtext, jptr, Fn); // Fn takes cur or cur_member
bool valid(jtext, opts = {}) noexcept;
result<micron::string> compact(jtext, opts = {});
result<micron::string> pretty(jtext, u8 indent = 2, opts = {});
result<micron::string> reformat(jtext, style, opts = {});
// writing
constexpr usize write_bound(const doc &, style = {}) noexcept; // O(1) with .with_write_bound
constexpr max_t write_into(const doc &, wbytes, style = {}) noexcept;
fjson write(const doc &, style = {}); // owning
micron::string write_str(const doc &, style = {});
constexpr usize minify_bound(usize n) noexcept; // == n
constexpr max_t minify(bytes in, wbytes out, opts = {}) noexcept;
result<fjson> minify(bytes, opts = {});
result<micron::string> minify_str(bytes, opts = {});
// subtree writers
constexpr usize write_bound(val, style = {}) noexcept;
constexpr max_t write_into (val, wbytes, style = {}) noexcept;
micron::string write_str (val, style = {});
fjson write (val, style = {});
// json <-> micron containers (runtime only)
using object_map = micron::hswiss<micron::string, pun>;
using array_vec = micron::vector<pun>;
result<object_map> to_map(val); result<object_map> to_map(cur);
result<array_vec> to_vector(val); result<array_vec> to_vector(cur);
template <pun_map M> error to_map_into(val|cur, M &);
template <pun_seq V> error to_vector_into(val|cur, V &);
constexpr usize map_slots(val) noexcept;
template <pun_map M> result<micron::string> to_json(const M &);
template <pun_seq V> result<micron::string> to_json_seq(const V &);
template <pun_map M> error write_map(builder &, const M &);
template <pun_seq V> error write_seq(builder &, const V &);
void write_pun(builder &, const pun &); // one value, any kind, into a builder
struct key_view { key_view(strv); strv view() const noexcept; /* ... */ };
class builder {
builder(); explicit builder(micron::string &&reuse); // recycle the buffer
error err() const noexcept;
builder &obj(); builder &arr(); builder &end();
builder &key(strv); // + const char*, is_string
builder &value(strv); // + const char*, is_string
builder &value(i64/u64/i32/u32/f64/bool);
builder &null();
builder &raw(strv json); // preserialized, trusted verbatim
template <class V> builder &kv(key, V v);
strv out() noexcept; // empty unless balanced and clean
micron::string take() noexcept; // move the buffer out, reset
};
} // namespace cjson
namespace cjson::ct { // comptime consteval
template <usize N> struct str; // NTTP carrier for a json literal
template <str S, opts O = {}> consteval bool validate();
template <str S, opts O = {}> consteval auto minify(); // -> bytes<N>
template <str S, opts O = {}> consteval auto parse(); // -> tree<NV, NS>
template <auto &Tree, style St = {}> consteval auto write(); // -> bytes<N>examples/ has nine programs covering each layer:
01_quickstart.cpp |
parse → read → build → write, end to end |
02_dom.cpp |
doc/val, getters, iteration, pointers, lifetimes |
03_ondemand.cpp |
iterate, scratch reuse, and the borrowing rules |
04_functional.cpp |
the whole fp layer, laziness made visible |
05_oneshot.cpp |
get/exists/each/pretty and friends |
06_build_write.cpp |
builder, write, minify, the sticky error |
07_comptime.cpp |
ct:: — almost entirely static_assert |
08_strings.cpp |
every text flavour through every entry point |
09_porcelain.cpp |
pun, to_map, and the micron container fns |
CJSON_DEPTH_LIMIT // nesting cap; default 1024, 0 folds the counter away entirelyHeader-only. micron core headers must be reachable as <micron/...>; requires C++26 (GCC 16+).
An easy to use script has been provided to fetch micron, run cd include/ && bash fetch_micron.sh to clone micron locally, then run any of the prebaked build_.*.sh scripts.
./build_benches.sh
./build_examples.sh
# if you want to use duck (build tool, you need to compile it via tools/src/main.cc in micron)
duck batch parallel build.duck # tools et al
duck batch parallel tests.duck # snowball suites (exit 1 == pass per binary)
duck batch parallel examples.duck # eight examples
scripts/ctbuild # comptime stress tier (raised constexpr limits)
# to run benchmarks
duck batch paralle benches.duck # benches
or ./build_benches.sh
scripts/fetch_corpus # wide-net corpus into sample/web/
scripts/vsbuild benches/corpus_vs.cpp # head-to-head against six libraries
taskset -c 0 ./bin/corpus_vs > benches/results/corpus_vs.txt
scripts/chart_corpus benches/results/corpus_vs.txt --headline
scripts/chart_corpus benches/results/corpus_vs.txt --all-metrics
scripts/chart_corpus benches/results/corpus_vs.txt --headline --mode github
scripts/chart_vs benches/results/parse_vs.txt # same style, parse_vs/write_vs
scripts/chart_vs benches/results/write_vs.txt --mode github
scripts/snapshot <bench>
A full corpus_vs sweep is long, nlohmann and boost.json are two orders of magnitude
slower than cjson on the object-heavy corpora. Pass only= to reduce fields benchmarked.
taskset -c 0 ./bin/corpus_vs parse only=twitter,canada,numbers
- No runtime CPU dispatch by design: a binary built with AVX2 requires AVX2.
- The lazy
fplayer is left-fold only - A warm scratch holds memory proportional to the largest document it has seen until
release(). - The porcelain fns (
pun,to_map,to_vector) are runtime-only. - Mutation invalidates handles on structural edits.
- Erasing a member does not reclaim its pool bytes.
- On the on-demand path, object keys marshalled by
to_mapare raw —\uescapes are not decoded, matchingcur::str_raw(). - Depends on the micron core library as its sole dependency.
MIT License — see LICENSE.