A lazy JSON parser for the D programming language. Parses only what you access: no wasted work.
- Lazy parsing: only the fields you access get parsed. Perfect for extracting a few values from large JSON payloads
- Eager mode: call
parseAll()for a fully-parsed tree when you need everything - Insertion-order preservation: object keys always iterate in the original JSON order
- Fluent access API: variadic arguments and JSON pointer paths (
/a/b/0) - JSONPath queries:
select("$..price")returns every matching node by reference, parsing only what the query needs - Callback walker: single pass over the document with JSONPath-selected callbacks, no tree built
- Safe access:
.safe!T()returns a result with a.foundflag instead of throwing - Resumable Streaming Parsing: append data to a partial JSON object and resume parsing seamlessly
- Binding: bind D structs and classes to JSON objects and arrays
- Mutation: set values, add keys, remove branches, then serialize back to JSON
- Interop with
std.json: convert to/fromstd.json.JSONValuewhen needed - Strict JSON compliance: passes 283/283 tests from the JSONTestSuite
- Performance: faster than
std.json
djson is rare in the training data of the models: a model left to guess mixes in the API of
std.json or of other libraries. Give it the reference instead:
- SKILL.md: the rules that are easiest to get
wrong, as a skill. AGENTS.md is the same text
without the front matter, for tools that want a rules file (
AGENTS.md,CLAUDE.md,.cursorrules, ...). - llms-full.txt: the whole API; llms.txt: a short overview.
The easiest way: ask your agent to do it.
Install the skill at https://trikko.github.io/djson/SKILL.md. It is the reference for djson, the D JSON library I am using.
Or by hand: a skill is a folder with SKILL.md in it (llms-full.txt next to it saves a download).
| Tool | For all projects | For one project |
|---|---|---|
| Claude Code | ~/.claude/skills/djson/ |
.claude/skills/djson/ |
| Antigravity (IDE, 2.0) | ~/.gemini/config/skills/djson/ |
.agents/skills/djson/ |
| Antigravity CLI | ~/.gemini/antigravity-cli/skills/djson/ |
.agents/skills/djson/ |
| Gemini CLI | ~/.gemini/skills/djson/ |
.gemini/skills/djson/ |
| Codex | ~/.agents/skills/djson/ |
.agents/skills/djson/ |
For example, for Claude Code:
mkdir -p ~/.claude/skills/djson && cd ~/.claude/skills/djson
curl -fsSLO https://trikko.github.io/djson/SKILL.md
curl -fsSLO https://trikko.github.io/djson/llms-full.txtThe skill is loaded when the task is about djson or JSON in D; in Claude Code you can also
call it with /djson.
dub add djson
import djson;
auto json = parseJSON(`{
"name": "djson",
"version": 1,
"features": ["lazy", "fast", "safe"],
"config": {"debug": false, "maxDepth": 64}
}`);
// Access values: only the accessed fields are parsed
string name = json.get!string("name"); // "djson"
int ver = json.get!int("version"); // 1
string feat = json.get!string("features", 1); // "fast"
bool dbg = json.get!bool("config", "debug"); // false// Lazy parse: returns immediately, parses on demand
auto json = parseJSON(`{"key": "value"}`);
// Eager parse: fully parses the entire tree in one pass
json.parseAll();DJSON supports parsing partial JSON strings from a stream. If the parser hits the end of the provided string while a value is still being read, it throws a JSONPartialException. You can then append more data and resume parsing exactly where it left off.
// Start with a partial JSON string
auto json = parseJSON(`{"hello": "world", "partial" :`);
// Accessing a completed key works
writeln(json.get!string("hello")); // "world"
// Accessing an incomplete key throws JSONPartialException
try {
json.get!string("partial");
} catch (JSONPartialException e) {
// Current chunk is incomplete, need more data
}
// Append the rest of the stream
json.appendData(`"world"}`);
// Now it works!
writeln(json.get!string("partial")); // "world"This works by leveraging the lazy nature of the parser: DJSON only resumes parsing for the node that was actually interrupted.
auto json = parseJSON(`{"user": {"name": "Alice", "age": 30}, "tags": ["admin"]}`);
// Direct access
string name = json.get!string("user", "name"); // variadic path
int age = json.get!int("user", "age");
// JSON pointer syntax
string name2 = json.get!string("/user/name");
string tag = json.get!string("/tags/0");
// Safe access (never throws)
auto result = json.safe!string("user", "email");
if (result.found) {
writeln(result.value);
} else {
writeln("not found");
}
// With default fallback
string email = json.safe!string("user", "email").or("n/a");
// Operator []
auto user = json["user"];
string n = user["name"].get!string;auto json = parseJSON(`{"a": 1, "b": null, "list": [10, 20]}`);
json.has("a") // true
json.has("missing") // false
json.has("a", "sub") // false: "a" is not an object
json.has("/list/0") // true: JSON pointer
json.has("list", 1) // true: variadic with index
// Null checking
json["b"].isNull // true
json["a"].isNull // false// Array iteration
auto arr = parseJSON(`[10, 20, 30]`);
foreach (el; arr) {
writeln(el.get!int);
}
// Array with index
foreach (size_t i, el; arr) {
writefln("[%d] = %d", i, el.get!int);
}
// Object iteration: preserves insertion order
auto obj = parseJSON(`{"z": 1, "a": 2, "m": 3}`);
foreach (string key, val; obj) {
writefln("%s = %d", key, val.get!int);
}
// Output: z = 1, a = 2, m = 3 (insertion order, not alphabetical)select returns the nodes matching a JSONPath expression. Only the parts of the document needed by the query are parsed. The path-based methods (get, has, safe, set, append) keep using JSON Pointers, so keys such as "$ref" are never ambiguous.
auto json = parseJSON(`{"store": {"book": [
{"title": "Moby Dick", "price": 8.99},
{"title": "The Lord of the Rings", "price": 22.99}
]}}`);
foreach (ref v; json.select("$.store.book[*].title"))
writeln(v.get!string);
// The result is a range: it works with std.algorithm
import std.algorithm : map, sum;
double total = json.select("$..price").map!(v => v.get!double).sum;
// Negative indices and slices
auto last = json.select("$.store.book[-1]")[0];
auto reversed = json.select("$.store.book[::-1]");
// Paths of the selected nodes
foreach (path, ref v; json.select("$..price"))
writeln(pathToString(path), " = ", v); // $['store']['book'][0]['price'] = 8.99
// Nodes are returned by reference: change them in place...
foreach (ref price; json.select("$..price"))
price = JValue(price.get!double * 0.9);
// ...or remove them all at once
json.select("$..price").remove();Supported syntax: $, .name, ['name'], [n] (negative counts from the end), [*], .*, descendants (..name, ..*, ..[n]), unions (['a','b'], [0,2]) and slices ([1:5], [::-1]). Filter expressions ([?...]) are not supported yet. select also accepts a JSON Pointer (/store/book/0).
Results locate each node by its position, so they stay valid while you read other parts of the document or change values. Removing members or elements invalidates the results that depend on them: accessing one throws JSONException.
select works on partial JSON too: it returns the nodes available so far and isComplete tells whether more may match once the rest of the stream arrives. Nodes whose value is still truncated are included, and reading them throws JSONPartialException, like get.
auto json = parseJSON(`{"users": [{"name": "Alice"}, {"name": "Bo`);
auto names = json.select("$.users[*].name");
names.length; // 2: "Alice" and the truncated "Bo…"
names.isComplete; // false: more users may follow
json.appendData(`b"}, {"name": "Carol"}]}`);
json.select("$.users[*].name").isComplete; // true: Alice, Bob, CarolSelectors counting from the end ([-1], [::-1]) select nothing until the array is complete.
walkJSON reads the document once, without building a tree, and calls a callback for each node selected by a JSONPath expression (starting with $) or a JSON Pointer (starting with /, same rules as get). Subtrees that no expression can reach are skipped without decoding them, which makes it the fastest way to extract a few fields from large payloads.
double x = 0, y = 0;
size_t n;
text.walkJSON!(
"$.coordinates[*].x", (double v) { x += v; n++; },
"$.coordinates[*].y", (double v) { y += v; },
);- The parameter type selects the conversion, like
get!T(double,long,string,bool,JValue, ...). Untyped lambdas receive aJValue. - Selected objects and arrays are passed as lazy
JValues, so you only pay for what you read inside them. They are reported after the callbacks for their own descendants. - An optional second parameter receives the path of the node:
json.walkJSON!("$..price", (double v, const(PathItem)[] path) {
writeln(pathToString(path), " = ", v); // $['store']['book'][0]['price'] = 8.95
});- Return
WalkControl.stopfrom a callback to end the traversal early. - When several expressions select the same node, their callbacks run in declaration order.
- JSON Pointers and JSONPath can be mixed in the same call:
text.walkJSON!(
"/info", (string s) { ... }, // JSON Pointer: exactly one node
"$.coordinates[*].x", (double v) { ... }, // JSONPath: many nodes
);Supported JSONPath syntax: $, .name, ['name'], [n], [*], .*, descendants (..name, ..*, ..[n]), unions (['a','b'], [0,2]) and slices with non-negative bounds ([1:5], [::2]). Filters ([?...]), negative indices and negative slice steps need data that is not available in a single pass: they are rejected at compile time, like any invalid expression. Use select when you need them.
As with lazy parsing, subtrees that are skipped are not validated.
auto json = parseJSON(`{"a": 1}`);
// Set by key
json["b"] = "hello";
json["c"] = true;
// Appending to arrays (auto-promotes primitives to arrays)
json["a"] ~= 2; // "a" became [1, 2]
// Set/Append by JSON pointer path (auto-vivifies intermediate nodes)
json.set(42, "/x/y/z");
json.append("item", "/list/tags"); // creates {"list": {"tags": ["item"]}}
// Overwrite nested objects
json["a"] = parseJSON(`{"nested": true}`);
// Remove keys and array elements
json.remove("b");
auto arr = parseJSON(`[1, 2, 3, 4, 5]`);
arr.remove(2); // removes element at index 2 → [1, 2, 4, 5]DJSON provides a concise way to build JSON structures manually using JSOB (JSON Object Builder) and JSAB (JSON Array Builder).
import djson;
// Build a complex JSON structure fluently
auto json = JSOB(
"name", "djson",
"version", 1,
"features", JSAB("lazy", "fast", "safe"),
"metadata", JSOB(
"author", "Andrea Fontana",
"tags", JSAB(1, 2, 3)
)
);
// Add to an existing object
json["new_key"] = "new_value";auto json = parseJSON(`{"b": 2, "a": 1}`);
json["c"] = 3;
// Compact
json.toJSON(); // `{"b":2,"a":1,"c":3}`
// Pretty-printed
json.toJSON(true);
// {
// "b": 2,
// "a": 1,
// "c": 3
// }import std.json;
import djson;
auto json = djson.parseJSON(`{"key": "value"}`);
JSONValue stdVal = json.toStdJSON();
assert(stdVal["key"].str == "value");
// And back: JSONValue objects are unordered, so members are sorted by key
JValue back = JValue(stdVal);
json["copy"] = stdVal; // JSONValue can be assigned directlyDJSON provides a binding system to convert between D structs/classes and JValue.
You can then use fromJSON!T to create a D object from a JValue, and toJSON to create a JValue from a D object.
import djson;
struct User {
// You can also apply @JSON to the whole struct/class.
// Here we apply it to a block of fields.
@JSON {
string name;
int age;
}
// This field is not included by default
string other;
}
// Convert from JValue to struct
auto json = parseJSON(`{"name": "Alice", "age": 30}`);
User u = fromJSON!User(json);
assert(u.name == "Alice");
assert(u.age == 30);
u.age = 35;
// Convert from struct to JValue
JValue v = toJSON(u);
assert(v["name"].get!string == "Alice");The binding system can be customized using User Defined Attributes (UDAs):
@JSON: If applied to a struct or class, all public fields are included by default. If applied to a field, that field is included even if it's not public or the parent isn't marked with@JSON. It also supports custom paths via variadic arguments or JSON pointers (e.g.,@JSON("path", "to", "key"),@JSON("/path/to/key")).@JSONIgnore: Explicitly exclude a field from binding.@JSONOptional: If the field is missing in the JSON input,fromJSONwill not throw an exception and will leave the field with its default.initvalue. Supports the same path mapping as@JSON.@JSONPreProcess!func: Apply a custom transformation function when reading from JSON. The function must have the signatureFieldType func(JValue v).@JSONPostProcess!func: Apply a custom transformation function when writing to JSON. The function must have the signatureJValue func(FieldType v).
import djson;
import djson.binding;
import std.string : toUpper;
@JSON
struct Config {
// Rename field in JSON
@JSON("max_threads") int threads;
// Optional field with default value
@JSONOptional string host = "localhost";
// Ignore this field
@JSONIgnore string internalKey;
// Custom pre-processing (e.g., uppercase a string)
@JSONPreProcess!((v) => v.get!string.toUpper)
string category;
}
auto json = parseJSON(`{"max_threads": 8, "category": "production"}`);
Config cfg = fromJSON!Config(json);
assert(cfg.threads == 8);
assert(cfg.host == "localhost");
assert(cfg.category == "PRODUCTION");@JSON (and @JSONOptional) supports variadic arguments and JSON pointers to bind D fields directly to nested JSON structures. You can also bind entire sub-structs for a more organized data model.
@JSON
struct Profile {
string name;
int level;
}
struct GameData {
// Bind to a deep path using JSON Pointer
@JSON("/server/status/code")
int statusCode;
// Bind using variadic segments, including array indices
@JSON("players", 0, "name")
string firstPlayerName;
// Bind using JSON Pointer for the second player
@JSON("/players/1/name")
string secondPlayerName;
// Bind an entire substructure
@JSON
Profile leader;
}
auto json = parseJSON(`{
"server": {"status": {"code": 200}},
"leader": {"name": "Alice", "level": 10},
"players": [
{"name": "Bob", "level": 5},
{"name": "Charlie", "level": 8}
]
}`);
GameData data = fromJSON!GameData(json);
assert(data.statusCode == 200);
assert(data.firstPlayerName == "Bob");
assert(data.secondPlayerName == "Charlie");
assert(data.leader.name == "Alice");
// Serialization preserves the deep structure
JValue v = toJSON(data);
assert(v.get!int("/server/status/code") == 200);
assert(v.get!string("players", 0, "name") == "Bob");# Run unit tests
dub test
# Run with LDC2 for best performance
dub test --compiler=ldc2
# Run benchmarks (in external_tests/) against std.json
cd external_tests
./run_benchmark.d # JSONTestSuite benchmark
./run_real_bench.d # Real-world benchmark (auto-downloads data)
./run_speed_test_large.d # Large string benchmark
./run_nst_suite.d # JSON compliance test suite
./run_compare.d # Value comparison with std.jsonUsing djson? I'd love to hear what you're building with it, or what's missing. Write to me: the address is just oss, at the domain of my website.
djson is built in my spare time. If it's useful to you or your company, consider sponsoring me on GitHub or buying me a beer on PayPal ❤️
MIT: see LICENSE for details.