RapidJSON (C++)
The fast C++ JSON standard from before simdjson — a header-only, self-contained parser/generator offering a dual SAX (event) and DOM (tree) API, a template-specialized recursive-descent reader, destructive in-situ zero-copy parsing, and a pool allocator, with SIMD used only as a narrow optimization (whitespace skipping and string scanning), not as the parsing strategy.
| Field | Value |
|---|---|
| Language | C++ (header-only, no STL/BOOST dependency) |
| License | MIT (source); bin/jsonchecker/ alone is under the JSON license |
| Repository | Tencent/rapidjson |
| Documentation | rapidjson.org · doc/sax.md · doc/dom.md · doc/internals.md |
| Key authors | Milo Yip (miloyip) et al.; © 2015 THL A29 Limited, a Tencent company, and Milo Yip |
| Category | High-performance JSON (C++, partial SIMD) |
| Algorithm / grammar class | Recursive-descent DOM builder + SAX event parser; strict RFC 7159 / ECMA-404 JSON (optional relaxed modes) |
| Lexing model | Scannerless, character-at-a-time recursive descent; SIMD accelerates whitespace-skip and string-scan only |
| Latest release | v1.1.0 (2016-08-25) |
NOTE
RapidJSON is a single-grammar parser, like simdjson — not a generator or combinator toolkit. Its place in this survey is as the reference point the SIMD generation measured itself against: simdjson's paper reports parsing "4x faster than RapidJSON" and using "a quarter or fewer instructions than a state-of-the-art reference parser like RapidJSON" (simdjson). Reading the two back to back is the sharpest available before/after picture of what whole-input SIMD changed. It shares the fast-JSON category with simd-json, sonic-rs, and yyjson.
Overview
What it solves
RapidJSON is "a fast JSON parser/generator for C++ with both SAX/DOM style API" (readme.md). It set the practical performance bar for C++ JSON in the mid-2010s by pairing a tightly template-specialized recursive-descent parser with aggressive memory discipline. The readme.md summarizes the four pillars:
"RapidJSON is small but complete. It supports both SAX and DOM style API. … RapidJSON is fast. Its performance can be comparable to
strlen(). It also optionally supports SSE2/SSE4.2 for acceleration. … RapidJSON is self-contained and header-only. It does not depend on external libraries such as BOOST. It even does not depend on STL. … RapidJSON is memory-friendly. Each JSON value occupies exactly 16 bytes for most 32/64-bit machines (excluding text string)." —readme.md
It targets strict compliance — "in full compliance with RFC7159/ECMA-404, with optional support of relaxed syntax" (readme.md) — with full Unicode: "Unicode-friendly. It supports UTF-8, UTF-16, UTF-32 (LE & BE), and their detection, validation and transcoding internally" (readme.md).
Design philosophy
Three ideas run through the codebase:
Static binding over virtual dispatch. The SAX
Readerand its userHandlerare bound at compile time through templates, so event calls inline: "RapidJSON uses templates to statically bind theReadertype and the handler type, instead of using classes with virtual functions. This paradigm can improve performance by inlining functions" (doc/sax.md). Parse flags are non-type template parameters, so "C++ compiler can generate code which is optimized for specified combinations, improving speed, and reducing code size" (doc/dom.md).SAX is the substrate; DOM is built on it. The DOM
Documentis itself a SAXHandler: "The DOM style API (rapidjson::GenericDocument) is actually implemented with SAX style API (rapidjson::GenericReader). SAX is faster but sometimes DOM is easier" (doc/features.md).Never copy what you can point at. In-situ parsing decodes strings in place, short-string optimization inlines small strings into the
Value, and the defaultMemoryPoolAllocatorbump-allocates and never frees individually. The goal throughout is cache coherence and minimal allocation.
RapidJSON is deliberately lean: it compiles "Without C++ exception, RTTI" and includes only <cstdio>, <cstdlib>, <cstring>, <inttypes.h>, <new>, <stdint.h> (doc/features.md).
How it works
The SAX core — GenericReader → Handler
Reader (a typedef of GenericReader) "parses a JSON from a stream. While it reads characters from the stream, it analyzes the characters according to the syntax of JSON, and publishes events to a handler" (doc/sax.md). The handler is a concept — any type exposing the fourteen event methods:
concept Handler {
bool Null();
bool Bool(bool b);
bool Int(int i); bool Uint(unsigned i);
bool Int64(int64_t i); bool Uint64(uint64_t i);
bool Double(double d);
bool RawNumber(const Ch* str, SizeType length, bool copy);
bool String(const Ch* str, SizeType length, bool copy);
bool StartObject(); bool Key(const Ch* str, SizeType length, bool copy);
bool EndObject(SizeType memberCount);
bool StartArray(); bool EndArray(SizeType elementCount);
};(include/rapidjson/reader.h) On a number the reader "chooses a suitable C++ type mapping" and calls exactly one of Int/Uint/Int64/Uint64/Double (doc/sax.md). Every event returns bool: "If the handler encounters an error, it can return false to notify the event publisher to stop further processing" — placing the reader in an error state with code kParseErrorTermination (doc/sax.md). Because Reader, Writer (the SAX generator), and Document all speak the same event vocabulary and none depend on the others, they chain freely: piping a Reader straight into a Writer removes whitespace (condense), into a PrettyWriter reformats (pretty), and an intermediate filter can transform events on the fly (capitalize) (doc/sax.md, doc/internals.md).
The DOM layer — GenericDocument and GenericValue
Value (= GenericValue<UTF8<>>) and Document (= GenericDocument<UTF8<>>) are typedefs of templates parameterized on Encoding and Allocator (doc/dom.md). Document is a Handler: "Document is a handler which receives events from a reader to build a DOM during parsing" (doc/sax.md). The inverse direction is Value::Accept(Handler&), which "is responsible for publishing SAX events about the value to the handler" (doc/dom.md) — this is why stringifying a DOM is written d.Accept(writer).
Value is a variant packed into 16 bytes. From doc/internals.md:
_"
Valueis a [variant type]. In RapidJSON's context, an instance ofValuecan contain 1 of 6 JSON value types. This is possible by usingunion. EachValuecontains two members:union Data data_and aunsigned flags*. Theflags*indicates the JSON type, and also additional information."_
The flags_ word carries both a sequential type tag and redundant capability bits (kIntFlag, kUintFlag, kInt64Flag, kDoubleFlag, …) so that IsNumber() is a single bit-test and integers auto-widen: "An Int is always an Int64, but the converse is not always true" (doc/internals.md).
Short-string optimization
Small strings live inside the 16-byte Value, avoiding a heap allocation and a pointer chase (doc/internals.md):
_"Excluding the
flags_, aValuehas 12 or 16 bytes (32-bit or 64-bit) for storing actual data. Instead of storing a pointer to a string, it is possible to store short strings in these space internally. For encoding with 1-byte character type (e.g.char), it can store maximum 11 or 15 characters string inside theValuetype."_
A neat trick stores MaxChars - length as the in-band length byte "to store 11 characters with trailing \0" and improves cache coherence (doc/internals.md).
In-situ parsing — destructive zero-copy
The signature RapidJSON idea. ParseInsitu(Ch* str) "decodes those JSON string at the place where it is stored. It is possible in JSON because the length of decoded string is always shorter than or equal to the one in JSON" (doc/dom.md). Decoding an escape (\n, s) only ever shrinks the text, so the result fits over the original bytes; an escape-free string like "msg" is handled by simply overwriting its closing quote with '\0' (doc/dom.md). The DOM's string Values then point directly into the caller's mutated buffer — the String() event fires with copy = false (doc/sax.md). Formally it is an O(1) auxiliary space algorithm (doc/dom.md):
"In situ parsing minimizes allocation overheads and memory copying. Generally this improves cache coherence, which is an important factor of performance in modern computer."
The cost is a sharp usage contract: the API takes char* not const char*, the whole JSON must be in memory, source and target encodings must match, and "The buffer need to be retained until the document is no longer used" (doc/dom.md) — dangling pointers otherwise. It suits "short-term JSON that only need to be processed once, and then be released" — deserializing to C++ objects, handling web requests (doc/dom.md).
MemoryPoolAllocator — bump-allocate, never free
The default DOM allocator "allocate but do not free memory. This is suitable for building a DOM tree" (doc/internals.md). Internally it "allocates chunks of memory from the base allocator (by default CrtAllocator) and stores the chunks as a singly linked list," serving requests from (1) an optional user-supplied buffer, then (2) the current chunk, then (3) a freshly allocated chunk (doc/internals.md). A user buffer — stack array or static scratch — can make a parse allocation-free entirely: "If the total size of allocation is less than 4096+1024 bytes during parsing, this code does not invoke any heap allocation … at all" (doc/dom.md). The alternative CrtAllocator wraps malloc/realloc/free and is "far less efficient" but supports piecemeal deallocation (doc/dom.md).
The one place SIMD lives — SkipWhitespace_SIMD
This is the crux of RapidJSON's contrast with simdjson: SIMD is a peephole optimization, not the parser. It accelerates whitespace skipping (and, similarly, string-content scanning), while the parse itself stays scalar and character-at-a-time. From doc/internals.md:
"To accelerate this process, SIMD was applied to compare 16 characters with 4 white spaces for each iteration. Currently RapidJSON supports SSE2, SSE4.2 and ARM Neon instructions for this. And it is only activated for UTF-8 memory streams, including string stream or in situ parsing."
The SSE4.2 path uses one _mm_cmpistri (pcmpistrm) per 16 bytes against the 4-char whitespace set, returning the index of the first non-whitespace (include/rapidjson/reader.h):
static const char whitespace[16] = " \n\r\t";
const __m128i w = _mm_loadu_si128(reinterpret_cast<const __m128i *>(&whitespace[0]));
for (;; p += 16) {
const __m128i s = _mm_load_si128(reinterpret_cast<const __m128i *>(p));
const int r = _mm_cmpistri(w, s, _SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT | _SIDD_NEGATIVE_POLARITY);
if (r != 16) // some of characters is non-whitespace
return p + r;
}The SSE2 fallback does four _mm_cmpeq_epi8 compares OR'd together, then _mm_movemask_epi8 + a bit-scan for the first mismatch; a NEON variant mirrors it (include/rapidjson/reader.h). The same _mm_cmpeq_epi8/_mm_max_epu8 pattern scans string bodies for ", \, and control bytes (< 0x20) during ParseStringToStream (include/rapidjson/reader.h). All of this is compile-time gated by RAPIDJSON_SSE2 / RAPIDJSON_SSE42 / RAPIDJSON_NEON: "these are compile-time settings. Running the executable on a machine without such instruction set support will make it crash" (doc/internals.md) — there is no runtime CPU dispatch, unlike simdjson. A historical page-boundary bug (a _mm_loadu_si128 reading past '\0' across a protected page, crashing ~1 in 500,000) was fixed by first advancing to the next aligned address, then using aligned reads (doc/internals.md).
Number parsing
By default numbers use internal::StrtodNormalPrecision(), which "has maximum 3 ULP error" and is fast (doc/internals.md). kParseFullPrecisionFlag switches to internal::StrtodFullPrecision(), which tries three methods in order — a decimal fast-path, a DIY-FP implementation (as in Google's double-conversion), and Clinger's Big-Integer method — falling through on failure (doc/internals.md). Generation is symmetric: a header-only Grisu2 for double-to-string ("always accurate … in most of cases it produces the shortest (optimal) string representation") and branchlut for integer-to-string (doc/internals.md).
Encodings & transcoding
GenericReader<SourceEncoding, TargetEncoding, Allocator> decouples the stream encoding from the emitted-string encoding, so a UTF-8 stream can produce UTF-16 String() events (doc/sax.md). During transcoding "the source string is decoded into Unicode code points, and then the code points are encoded in the target format," validating the byte sequence and failing with kParseErrorStringInvalidEncoding on a bad sequence (doc/dom.md). When source and target encodings match, validation is skipped unless kParseValidateEncodingFlag is set (doc/dom.md).
Algorithm & grammar class
RapidJSON parses exactly one grammar — strict JSON (RFC 7159 / ECMA-404), with optional relaxed extensions (comments, trailing commas, NaN/Inf), and offers two engines for it:
- Recursive-descent (default).
Parsedispatches throughParseValue→ParseObject/ParseArray/ParseString/ParseNumber, recursing on the C++ call stack. "Recursive parser is faster but prone to stack overflow in extreme cases" (doc/features.md). - Iterative (
kParseIterativeFlag). "The iterative parser is a recursive descent LL(1) parser implemented in a non-recursive manner" (doc/internals.md). Left-factoring thevalues/membersproductions makes the grammarLL(1); the FIRST/FOLLOW parsing table is then "encoded in a state machine" with extra states for array/object element counting, giving "constant complexity in terms of function call stack size" (include/rapidjson/reader.h,doc/internals.md).
Ambiguity does not arise — JSON is LL(1) with single-token lookahead, exactly the property the iterative parser's table exploits (see top-down / recursive descent and formal languages). This is the character-at-a-time recursive-descent end of the design space — the antithesis of simdjson's whole-input SIMD structural indexing.
Interface & composition model
There is no grammar DSL or combinator — the surface is JSON-in, events-or-tree-out — but two composition axes stand out:
| API | Shape | When |
|---|---|---|
| SAX | GenericReader → user Handler; Writer/PrettyWriter consume the same events | Streaming, filtering, custom in-memory structures, min RAM |
| DOM | GenericDocument builds a GenericValue tree; Value::Accept re-emits events | Random access, mutation, re-serialization |
The Handler concept is the composition primitive: because Reader, Writer, Document, and any user filter all implement it, they form pipelines (Reader → Filter → Writer) with no shared base class (doc/internals.md). The DOM tree is built fully (unlike simdjson's lazy On Demand), but a SAX handler can build a custom, smaller structure — the messagereader example populates a std::map directly, "eliminat[ing] building of DOM, thus reducing memory and improving performance" (doc/sax.md).
Performance
Performance is the reason RapidJSON existed; the concrete posture:
- Throughput. README claims performance "comparable to
strlen()" (readme.md); it long topped the nativejson-benchmark collection. Relative to the SIMD generation it is now the baseline: simdjson reports ~4× higher throughput and, per its paper, RapidJSON at 18.7 instructions/byte vs simdjson's 8.3 (simdjson). - In-situ zero-copy. No string allocation and no copy for escape-free strings; O(1) auxiliary memory (
doc/dom.md). - Allocation. Bump-pointer
MemoryPoolAllocatorwith optional user buffer → parses with zero heap allocation when the buffer suffices (doc/dom.md). - SIMD. Narrow — whitespace skip and string-body scan only, 16 bytes/iteration, compile-time-gated, no runtime dispatch (
doc/internals.md). The parse loop itself is scalar. - Value footprint. Exactly 16 bytes per
Valueon most machines; short strings inlined (readme.md,doc/internals.md). - Backtracking / memoization. None —
LL(1)recursive descent with single-token lookahead, one forward pass.
Error handling & recovery
RapidJSON is a strict, fail-fast validator — no recovery, no resynchronization. On the first violation it stops and records a ParseErrorCode plus a character offset: the DOM has HasParseError(), GetParseError(), and GetErrorOffset(), and "When there is an error, the original DOM is unchanged" (doc/dom.md). The error enum is granular — kParseErrorObjectMissColon, kParseErrorStringUnicodeSurrogateInvalid, kParseErrorNumberTooBig, kParseErrorTermination, and a dozen more (doc/dom.md) — and rapidjson/error/en.h maps codes to English messages, with localization left to the user. The offset is a raw character count: "Currently RapidJSON does not keep track of line number" (doc/dom.md). A handler returning false aborts the parse with kParseErrorTermination, which is how streaming validators reject early (doc/sax.md). There is no incremental reparse and no IDE-grade diagnostics — for that tolerant, error-recovering end of the space, see tree-sitter via the comparison. Multiple concatenated JSON documents in one stream are supported via kParseStopWhenDoneFlag (doc/dom.md).
Ecosystem & maturity
RapidJSON is a mature, widely-vendored Tencent open-source project (© 2015 THL A29 Limited, a Tencent company, and Milo Yip). Distribution is frictionless — header-only, "Just copy the include/rapidjson folder" (readme.md) — with vcpkg and CMake find_package(RapidJSON) integration. It ships a broad example set (DOM tutorial; SAX simplereader, condense, pretty, capitalize, messagereader, serialize, jsonx; schemavalidator; and advanced prettyauto, parsebyparts, filterkey) and a googletest-based unit + performance test suite (readme.md). Standards coverage beyond core JSON includes JSON Pointer (RFC 6901), JSON Schema Draft v4, Swagger v2 / OpenAPI v3.0 schema, and NPM compliance (doc/features.md). The v1.1.0 release (2016-08-25) added JSON Pointer, JSON Schema, relaxed syntax, C++11 range-based iteration, and shrank Value from 24 to 16 bytes on x86-64 (readme.md).
NOTE
The pinned tree carries a version discrepancy worth flagging: the readme.md claims "full compliance with RFC7159/ECMA-404," while doc/features.md says "fully RFC4627/ECMA-404 compliance." RFC 7159 obsoletes RFC 4627; the README is the newer, authoritative statement.
Strengths
- Battle-tested, header-only, dependency-free — no STL/BOOST, no exceptions/RTTI, trivial to vendor.
- Dual SAX + DOM on one event vocabulary — stream, filter, or build a tree; pipelines compose without a shared base class.
- In-situ destructive parsing — O(1) auxiliary memory, no string copies, cache-friendly for parse-once workloads.
- Aggressive memory discipline — 16-byte
Value, short-string inlining, bump-pointer pool allocator, optional zero-heap parsing via a user buffer. - Full Unicode — UTF-8/16/32 (LE & BE), detection, validation, and transcoding between stream and DOM encodings.
- Compile-time specialization — template-bound handler + non-type parse-flag template parameters inline the hot path.
- Accurate numbers on demand — Grisu2 output, optional full-precision (correctly-rounded) input parsing.
Weaknesses
- One grammar only — not a parsing toolkit; you cannot express another language. (By design.)
- SIMD is narrow and static — only whitespace/string scanning is vectorized, and the ISA is chosen at compile time with no runtime dispatch; a binary built for SSE4.2 crashes on a CPU without it. This is precisely the ceiling simdjson broke by making SIMD the whole parse.
- No error recovery, no incremental reparse — first-error-and-stop; offsets are character counts with no line numbers; wrong tool for an editor or LSP.
- In-situ's contract is sharp — mutates the caller's buffer, needs
char*, source/target encodings must match, and the buffer must outlive the DOM. - Recursive parser can stack-overflow on deeply nested input unless you opt into the iterative
LL(1)engine. MemoryPoolAllocatornever frees individually — great for parse-then-discard, wasteful for long-lived, heavily-mutated DOMs (useCrtAllocatorthere).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
SAX as substrate, DOM as a Handler on top | One event model powers streaming, filtering, tree-building, and re-serialization | DOM users pay for a generic event interface; SAX is faster but forces manual state-keeping |
| Template/static binding of reader + handler | Events inline; parse flags as template params specialize the hot path | Flags must be compile-time constants; more instantiations / code bloat |
In-situ destructive parsing (ParseInsitu) | O(1) extra memory, no string copies, cache-friendly | Mutates and pins the input buffer; char* API; encodings must match |
16-byte Value variant + short-string inlining | Small footprint, fewer allocations and pointer chases, better cache behavior | 11/15-char inline cap; bit-packed flags_ is intricate |
MemoryPoolAllocator (bump, no per-object free) | Fastest possible allocation for build-once DOMs; optional user buffer → zero heap | Cannot reclaim individual nodes; unsuited to churn-heavy DOMs |
| SIMD only for whitespace/string scan, compile-time ISA | Cheap, targeted win on a measured hot spot without rewriting the parser | No runtime dispatch (mis-targeted binary crashes); leaves the scalar parse as the bottleneck |
Recursive descent by default, iterative LL(1) opt-in | Recursion is fastest; iterative bounds stack for adversarial nesting | Default risks stack overflow; iterative table/state-machine is more complex |
| Strict validation, first-error-stop, char-offset diagnostics | Keeps the hot path simple and fast; rejects malformed input outright | No recovery, no line numbers, no incremental reparse — useless for editors/LSPs |
| Normal-precision floats by default, full-precision opt-in | Fast common path (≤ 3 ULP); correctness on demand | Default is not correctly-rounded; full precision is slower |
Sources
Tencent/rapidjson— GitHub repository · rapidjson.orgreadme.md— positioning, four pillars, v1.1.0 highlights, examplesdoc/sax.md—Reader/Handler/Writer, static binding, event pipelinedoc/dom.md—GenericValue/GenericDocument, in-situ parsing, allocators, parse errors, transcodingdoc/internals.md—Valuelayout, short-string opt,MemoryPoolAllocator,SkipWhitespace_SIMD, iterativeLL(1)parser, float parsingdoc/features.md— feature matrix, standards compliance, recursive vs iterativeinclude/rapidjson/reader.h—ParseFlag,Handlerconcept,SkipWhitespace_SIMD(SSE2/SSE4.2/NEON),ParseStringToStreamlicense.txt— MIT license (source); JSON license confined tobin/jsonchecker/- Related: umbrella · concepts glossary · comparison · simdjson ·
simd-json·sonic-rs·yyjson· Hyperscan · top-down / recursive descent · formal languages