# yyjson: a JSON parser in ANSI C that trades convenience for speed

> One .h file and one .c file, strict RFC 8259 parsing, a documented immutability rule and benchmark tables against simdjson and RapidJSON, with the README admitting where it loses to the newer on-demand parsers.

**ibireme/yyjson** — The fastest JSON library in C

- Repository: https://github.com/ibireme/yyjson
- Website: https://ibireme.github.io/yyjson/doc/doxygen/html/
- Stars: 3,883 · Forks: 345
- Language: C
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/ibireme-yyjson

## Two files and a C89 baseline

The integration story is the first thing the feature list mentions and it is worth taking literally: easy integration with just one .h and one .c file. There is no CMake requirement in the headline claim, no dependency tree, and no platform specific branches to resolve. You drop the pair into your build and include the header. The repository tree backs this up, with the implementation under src/, the documentation under doc/, a fuzz directory, a test directory and a CMakeLists.txt for people who want one anyway.

The portability claim is specific rather than aspirational. The README states compliance with ANSI C, dated C89, with no explicit SIMD. That is a deliberate constraint and it has consequences you can feel. Without hand-written vector instructions, yyjson gets its throughput from the ordinary things a scalar C parser can exploit: instruction level parallelism, a good branch predictor, and low penalty for misaligned memory access. The README says exactly that in its notes on how to get better performance, listing the three processor characteristics it prefers and suggesting clang as the compiler most likely to cooperate.

Version 0.13.0 adds a compile-time option to build without libc, aimed at WebAssembly, and a separate option to disable the file and file pointer read and write APIs. Both are the kind of thing that only matters if you are targeting an environment where the C standard library is not a given, and both are off by default.

## Reading a document in the shape most C code expects

The first sample in the README is a complete read. You parse with yyjson_read, pull the root with yyjson_doc_get_root, and then reach into it with typed getters:

```c
const char *json = "{\"name\":\"Mash\",\"star\":4,\"hits\":[2,2,1,3]}";

yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
yyjson_val *root = yyjson_doc_get_root(doc);
yyjson_val *name = yyjson_obj_get(root, "name");
printf("name: %s\n", yyjson_get_str(name));
printf("name length:%d\n", (int)yyjson_get_len(name));
```

Two details in that snippet carry most of the API's character. The getters are untyped functions taking a value, so the type checking that a tagged union in C would give you does not exist here; a mismatched getter is a bug the compiler will not catch. And the array access goes through a macro, yyjson_arr_foreach, rather than an index operator, because that limitation is listed first in the README's own limitations section.

The error model is worth noting as well. The README closes the sample with a comment that all functions accept NULL input and return NULL on error, so a NULL return is the single failure signal across the whole surface. When you want detail, you pass an error struct, and the file-reading sample prints the code, the message and the byte position.

```c
yyjson_read_flag flg = YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS;
yyjson_read_err err;
yyjson_doc *doc = yyjson_read_file("/tmp/config.json", flg, NULL, &err);
```

Those two flags relax the parser for configuration files that are not quite JSON. Turning them on is a decision about your input, not about the library.

## Writing goes through a separate mutable document type

Because a parsed document is immutable, writing starts from a different type. You create a mutable doc, build values into it, and serialize:

```c
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
yyjson_mut_val *root = yyjson_mut_obj(doc);
yyjson_mut_doc_set_root(doc, root);
yyjson_mut_obj_add_str(doc, root, "name", "Mash");
yyjson_mut_obj_add_int(doc, root, "star", 4);
yyjson_mut_doc_free(doc);
```

The mut_ prefix on every function is doing real work here. It marks the half of the API that allocates and modifies, and it is the half where your allocator choice matters, which is why the new function takes an allocator as its first argument and accepts NULL for the default.

The README also documents a route between the two halves: read a file into an immutable doc, then call yyjson_doc_mut_copy to get a mutable version and edit it. That is how the write-file sample removes null values from a loaded configuration. The cost of this split is that modification requires a copy, which the limitations section states plainly: JSON parsing results are immutable, requiring a mutable copy for modification.

Version 0.13.0 added a set of write_buf functions for serializing into a buffer without allocating, along with a YYJSON_WRITE_LOWERCASE_HEX flag for writing unicode escapes in lowercase and separate reader and writer depth limits as compile-time options.

## What the benchmark tables actually claim

The README publishes two tables and they are the reason the project has this reputation. On an AWS EC2 instance with an AMD EPYC 7R32 and gcc 9.3, parsing twitter.json, the in-situ yyjson variant reports 1.80 GB/s and simdjson reports 1.52 GB/s, with rapidjson in-situ at 0.77 and jansson at 0.05. Stringify tells a different story, with yyjson in-situ at 1.51 GB/s against simdjson at 0.61.

On an iPhone 12 Pro with an A14 and clang 12, the same file parses at 3.51 GB/s in-situ, and simdjson is at 2.19. The interactive charts linked from the table cover six platform and compiler combinations, including a Graviton2 ARM64 machine and several desktop configurations, with the note that the last update was 2020-12-12.

Two caveats sit right next to those numbers and both matter. The benchmark project is a separate repository, and the README concedes that simdjson's new on-demand API is faster when most JSON fields are known at compile time, conceding that the published comparison only checks the DOM API. So the fair statement is that yyjson leads on DOM parsing and on writing, while simdjson leads on a different access pattern. The tables also date from 2020, which is a long time in a field where both libraries have shipped since.

## Three limitations the project states before you hit them

The limitations section is short and it is the most useful part of the README, because each item predicts a design decision you will otherwise make by accident.

The first is the data structure. An array or object is stored as something like a linked list, which the README says makes accessing elements by index or key slower than iterating. This is the price of the contiguous, cache-friendly immutable layout, and it is also why the API is iterator-first. If your workload is a handful of fields per object and you touch each once, you will not notice. If it is one field out of two hundred, out of a large document, you will.

The second is duplicate keys. They are allowed in an object and their order is preserved, rather than being collapsed last-write-wins. That is a spec-compliance choice, since RFC 8259 does not forbid them, and it means a parser cannot silently discard a shadowed field if you care to detect the ambiguity.

The third is immutability, discussed above. There is no in-place set on a parsed document. Between the three, the linear key lookup is the one most likely to change your design.

## Beyond parsing: JSON Pointer, Patch and Merge Patch

The feature list advertises querying and modifying with three separate specifications: JSON Pointer for addressing, JSON Patch for a list of operations, and JSON Merge Patch for a partial document that means what it says. This is a larger scope than most parsers take, and it means yyjson can sit in the middle of a pipeline that has to apply a server-supplied change description rather than a bespoke function call.

Accuracy is claimed in a specific place too: the library can accurately read and write int64, uint64 and double numbers. Given that JSON has one number type, that is a real feature and it has a history, since the 0.13.0 changelog records fixes for integer truncation when parsing extremely large numbers and for an uninitialized read found by OSS-Fuzz.

Flexibility follows the same pattern of small accommodations for real inputs: unlimited nesting depth, support for embedded null characters, and non-null-terminated strings, which matters when the JSON sits inside a larger buffer. The 0.12.0 release added the full set of JSON5 relaxations as individual flags, including single-quoted strings, unquoted keys, extended number formats and extended escapes, plus one flag that turns on JSON5 wholesale. Strict by default, permissive by choice, one flag at a time.

## Conclusion

yyjson is a good fit when the JSON sits on a hot path in C or C++, when you cannot take an Apache-licensed dependency, and when you want a parser that refuses malformed input instead of guessing. The DOM API it ships is deliberately not the fastest thing available: the README says plainly that simdjson's newer on-demand API wins when most fields are known at compile time, and that the comparison it publishes only covers the DOM. The honest summary is that you get a fast, strict, portable parser that scales well across CPUs and Apple silicon, with three limitations you should read before designing around it: key lookup is linear, duplicate keys survive parsing, and parsing produces an immutable document. Start from the read example in the README, then read the data structures page before you assume indexed access is cheap.

## FAQ

### How is simdjson so fast?

simdjson uses explicit SIMD instructions to validate and parse many bytes at once, which is why its on-demand API can lead when the set of fields you read is fixed at compile time. yyjson takes the opposite approach, staying within ANSI C89 with no explicit SIMD, and instead relies on instruction level parallelism, branch prediction and low misaligned access penalties. The two reach different throughput profiles rather than one being a faster version of the other.

### Is orjson faster than JSON?

The comparison does not line up as written, since orjson is a Rust library and JSON is a data format. The yyjson benchmark tables compare C and C++ libraries instead: yyjson, simdjson, sajson, rapidjson, cjson and jansson, on an EC2 machine and on an iPhone 12 Pro, using the twitter.json dataset.

### What is the fastest C++ JSON library?

The published tables point to simdjson on parse throughput and to yyjson on stringify, with simdjson at 1.52 GB/s and yyjson in-situ at 1.80 GB/s on EC2, and at 2.19 and 3.51 GB/s respectively on an A14. Those numbers are from the DOM API only, dated 2020-12-12, and the README notes that simdjson's on-demand API is faster when most fields are known at compile time.

## Sources

- [ibireme/yyjson on GitHub](https://github.com/ibireme/yyjson)
- [License: MIT](https://github.com/ibireme/yyjson/blob/master/LICENSE)
- [Project website](https://ibireme.github.io/yyjson/doc/doxygen/html/)
- [README](https://github.com/ibireme/yyjson/blob/master/README.md)
- [Releases](https://github.com/ibireme/yyjson/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ibireme-yyjson
