Skip to content

Repository files navigation

djson logo djson

A lazy JSON parser for the D programming language. Parses only what you access: no wasted work.

Features

  • 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 .found flag 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/from std.json.JSONValue when needed
  • Strict JSON compliance: passes 283/283 tests from the JSONTestSuite
  • Performance: faster than std.json

Documentation

Using djson with an AI agent

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.txt

The skill is loaded when the task is about djson or JSON in D; in Claude Code you can also call it with /djson.

Installation

dub add djson

Quick Start

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

API Reference

Parsing

// Lazy parse: returns immediately, parses on demand
auto json = parseJSON(`{"key": "value"}`);

// Eager parse: fully parses the entire tree in one pass
json.parseAll();

Partial Streaming & Resumable Parsing

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.

Reading Values

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;

Checking Existence

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

Iteration

// 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)

Querying with JSONPath

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, Carol

Selectors counting from the end ([-1], [::-1]) select nothing until the array is complete.

Callback Walker (JSONPath / JSON Pointer)

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 a JValue.
  • 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.stop from 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.

Mutation

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]

Construction (Builders)

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";

Serialization

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
// }

Interoperability with std.json

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 directly

JSON Binding

DJSON provides a binding system to convert between D structs/classes and JValue.

Basic Usage

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");

UDAs for Customization

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, fromJSON will not throw an exception and will leave the field with its default .init value. Supports the same path mapping as @JSON.
  • @JSONPreProcess!func: Apply a custom transformation function when reading from JSON. The function must have the signature FieldType func(JValue v).
  • @JSONPostProcess!func: Apply a custom transformation function when writing to JSON. The function must have the signature JValue 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");

Deep Path Binding

@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");

Building & Testing

# 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.json

Feedback & support

Using 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 ❤️

License

MIT: see LICENSE for details.

About

Fast, lazy, zero-dependency JSON parser and serializer for D: JSONPath, streaming, struct binding.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Contributors

Languages