yyjson
June 28, 2026 ยท View on GitHub
Thin Nim bindings for yyjson, with a small idiomatic wrapper for fast DOM-style JSON parsing, navigation, mutation, and writing.
This package is not a drop-in replacement for std/json. It is meant for code
that wants yyjson's speed and memory profile without writing C-style Nim FFI at
every call site.
Status
yyjson is usable as a 1.0.x package. The public API focuses on:
- parsing JSON from strings and files
- object and array access
- typed value access for strings, numbers, booleans, nulls, raw numbers
- zero-copy
cstringaccess when you want it - JSON Pointer get/set/add/replace/remove helpers
- JSON Patch and JSON Merge Patch support
- JSON writing to strings and files
- mutable JSON document construction and editing
- ARC/ORC-friendly ownership
The wrapper intentionally does not expose every low-level yyjson C helper as a
public Nim API. The lower-level yyjson/private module exists for the wrapper
and tests, but it is not the package's stable user-facing contract.
Install
After the package is published to Nimble:
nimble install yyjson
From a local checkout:
nimble install
Then:
import yyjson
No system yyjson library is required. The package vendors yyjson.c and
yyjson.h and compiles yyjson automatically.
Ownership
JsonDoc and JsonMutDoc own yyjson documents. They are move-only handles.
Keep them in var variables and call close() exactly once, usually with
defer:
proc loadName() =
var doc = readJson("""{"name":"redis"}""")
defer:
doc.close()
echo doc.root()["name"].str()
Values such as JsonVal and JsonMutVal borrow from their owning document.
Do not use them after the document has been closed.
Quick Start
import yyjson
proc main() =
var doc = readJson("""{"name":"redis","tags":["7.4","latest"],"enabled":true}""")
defer:
doc.close()
let root = doc.root()
echo root["name"].str()
echo root["enabled"].bool()
for tag in root["tags"].items:
echo tag.str()
when isMainModule:
main()
Object Access
let root = doc.root()
if root.hasKey("name"):
echo root["name"].str()
if "name" in root:
echo root.getStr("name")
for key, value in root.pairs:
echo key, " = ", value.kind()
Missing keys and out-of-range indexes return nil values:
if root["missing"].isNil:
echo "not found"
Typed Values
echo root["name"].str()
echo root["enabled"].bool()
echo root["count"].int()
echo root["count"].uint64()
echo root["ratio"].float()
String access methods:
cstr()returns a zero-copycstringstr()copies to a NimstringgetCStr(key)returns a zero-copy field valuegetStr(key)copies a field value
Zero-copy pointers are valid only while the owning document is alive.
JSON Pointer
let value = doc.root().pointer("/metadata/name")
if not value.isNil:
echo value.str()
Strict pointer helpers raise JsonPointerError with yyjson's error code and
position:
try:
discard doc.root().pointerStrict("/missing/value")
except JsonPointerError as e:
echo e.code, " at ", e.pos
Write JSON
let compact = doc.writeJson()
let pretty = doc.writeJson(YYJSON_WRITE_PRETTY_TWO_SPACES)
writeJsonFile("out.json", doc)
writeJsonFile("tags.json", doc.root()["tags"])
Useful write flags include:
YYJSON_WRITE_PRETTYYYJSON_WRITE_PRETTY_TWO_SPACESYYJSON_WRITE_NEWLINE_AT_ENDYYJSON_WRITE_ESCAPE_UNICODEYYJSON_WRITE_ESCAPE_SLASHESYYJSON_WRITE_ALLOW_INF_AND_NANYYJSON_WRITE_INF_AND_NAN_AS_NULL
Mutable JSON
proc buildJson() =
var mutDoc = newJsonMutDoc()
defer:
mutDoc.close()
let root = mutDoc.newObject()
let tags = mutDoc.newArray()
tags.add(mutDoc.newString("7.4"))
tags.add(mutDoc.newString("latest"))
root.add("name", mutDoc.newString("redis"))
root.add("enabled", mutDoc.newBool(true))
root.add("tags", tags)
mutDoc.setRoot(root)
echo mutDoc.writeJson()
Mutable arrays and objects support add, replace, remove, clear, indexing, iteration, and JSON Pointer mutation helpers.
Errors
Parsing, writing, and strict pointer operations raise package-specific exceptions:
JsonReadErrorJsonWriteErrorJsonPointerError
These exceptions expose yyjson's error code and a human-readable reason.
try:
discard readJson("[1,]")
except JsonReadError as e:
echo e.code, " at byte ", e.pos, ": ", e.reason
Examples
nim c -r -p:src examples/parse.nim
nim c -r -p:src examples/harbor.nim -- vulnerabilities.json
The harbor example scans a Harbor vulnerabilities JSON export without
building a std/json.JsonNode tree.
Tests
nimble test
The test suite contains focused Nim API tests plus upstream-inspired yyjson
tests and fixtures under tests/upstream and tests/data/yyjson.
The fixture data is copied from yyjson's upstream test/data corpus. The tests
cover behavior that matters to this Nim package: parsing flags, writing flags,
errors, strings, numbers, allocators, JSON Pointer, JSON Patch, JSON Merge
Patch, mutable DOM operations, and selected low-level FFI boundaries.
The suite does not try to port every C-only upstream test. Internal tag/subtype checks, stack-allocated C value tests, and helpers that are not part of the public Nim API are intentionally left outside the 1.0.x scope.
Vendored yyjson
The package vendors:
src/yyjson/vendor/yyjson.c
src/yyjson/vendor/yyjson.h
src/yyjson/private.nim compiles the vendored yyjson.c automatically. If the
vendored yyjson version is updated, run nimble test before publishing a new
package version.
Why not std/json?
std/json is convenient and universal, but it builds a generic JsonNode tree.
That is not ideal when documents are large and only selected fields are needed.
yyjson is intended for cases like exporters, crawlers, scanners, API
gateways, and CLI tools where:
- JSON payloads are large
- only selected fields are needed
- memory pressure matters
- zero-copy string access is useful
Memory Profile Note
On one real Harbor vulnerabilities JSON file of about 10 MiB, peak RSS was:
| Parser | Peak RSS |
|---|---|
std/json.parseJson | ~82 MiB |
std/parsejson token parser | ~34 MiB |
gemmaJSON | ~38 MiB |
| yyjson C API | ~16 MiB |
These numbers are illustrative, not a benchmark guarantee. They depend on the compiler, allocator, OS, yyjson version, and input shape.
Publishing Notes
For the initial Nimble release, the package is intentionally conservative:
- public API first
- upstream fixture coverage for behavior that affects Nim users
- private FFI bindings only where needed by the wrapper or tests
- deeper low-level C API parity can be added in later versions
License
MIT for the Nim wrapper. Vendored yyjson keeps its upstream license.