Skip to content

sparkles:wired — JSON serialization

sparkles:wired maps D values to and from JSON by structural introspection — the mapping is derived from each type at compile time, with no schemas or code generation, and optional @Wire* attributes to tune wire names, casing, and representation.

Both directions are Expected-based and never throw: toJSON returns an Expected!(JSONValue, Exception) and fromJSON!T returns an Expected!(T, Exception), so a failure is a value you branch on rather than an exception you catch. The library builds on std.json for parsing and printing.

Installation

sh
dub add sparkles:wired
d
#!/usr/bin/env dub
/+ dub.sdl:
    dependency "sparkles:wired" version "*"
+/
sdl
dependency "sparkles:wired" version "*"
json
{
    "dependencies": {
        "sparkles:wired": "*"
    }
}

Decode JSON — fromJSON

fromJSON!T reads a std.json.JSONValue and reconstructs a T, recursing through arrays, objects, and nested aggregates. It returns an Expected!(T, Exception) whose .value holds the decoded result on success:

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "wired_from_json"
    dependency "sparkles:wired" version="*"
+/
import std.json : parseJSON;
import std.stdio : writeln;
import sparkles.wired : fromJSON;

struct Server
{
    string host;
    ushort port;
    string[] tags;
}

void main()
{
    auto json = parseJSON(`{ "host": "localhost", "port": 8080, "tags": ["web", "edge"] }`);
    Server server = json.fromJSON!Server.value;
    writeln(server);
}
Server("localhost", 8080, ["web", "edge"])

Encode values — toJSON

toJSON is the inverse: it walks a value and produces an Expected!(JSONValue, Exception), whose .value std.json renders (object keys are emitted in sorted order):

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "wired_to_json"
    dependency "sparkles:wired" version="*"
+/
import std.stdio : writeln;
import sparkles.wired : toJSON;

struct Server
{
    string host;
    ushort port;
    string[] tags;
}

void main()
{
    auto server = Server("localhost", 8080, ["web", "edge"]);
    writeln(server.toJSON.value.toPrettyString);
}
{
    "host": "localhost",
    "port": 8080,
    "tags": [
        "web",
        "edge"
    ]
}

Errors as values

Because decoding never throws, malformed input surfaces as the Exception payload of the returned Expected!(T, Exception) — branch on hasValue / hasError and inspect the failure as data. Decode errors carry a precise message, including, for enums, the set of names that would have matched:

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "wired_errors"
    dependency "sparkles:wired" version="*"
+/
import std.json : parseJSON;
import std.stdio : writeln;
import sparkles.wired : fromJSON;

enum Mode { off, on, automatic }

void main()
{
    // fromJSON never throws — it returns Expected!(T, Exception).
    auto good = parseJSON(`"on"`).fromJSON!Mode;
    writeln("value: ", good.hasValue, " ", good.value);

    auto bad = parseJSON(`"sideways"`).fromJSON!Mode;
    writeln("error: ", bad.hasError);
    writeln("       ", bad.error.msg);
}
value: true on
error: true
       Cannot decode Mode at $ from JSON string "sideways": expected one of: off, on, automatic

API

SymbolDescription
toJSON(value)Expected!(JSONValue, Exception)Encode a value; a failure is captured as the Exception payload (never throws).
fromJSON!T(JSONValue)Expected!(T, Exception)Decode a value; a failure is captured as the Exception payload (never throws).
readJSONFile!T(string path)Expected!(T, Exception)Read, parse, and decode a file; the error identifies the failing stage (read, parse, decode).
writeJSONFile(value, path, bool compact = false)Expected!(void, Exception)Encode and write to path atomically, creating parent directories; compact writes a single line.
@WireName("…")Field / enum-member UDA overriding the JSON wire name.
@WireCase(CaseStyle.…)Recase field / member names (e.g. snakeCase, kebabCase).
@WireRepr(Repr.…)Serialize an enum by member name (default) or underlying value.

Supported types

The same structural mapping covers a broad range of types, in both directions:

  • Scalarsbool, string, char, integral and floating-point types
  • Enums — by member name, or a @WireName / @WireCase / @WireRepr override
  • Arrays / slices — of any supported element type
  • Associative arrays — keyed by string or by an enum
  • Aggregates (struct) — field by field, under their member names
  • SumType — encoded as its active variant; decoding tries each variant in turn
  • Nullable!T / Optional!T — JSON null ⇄ the empty value
  • Ternary — JSON null / true / false
  • SysTime — an ISO-8601 extended string
  • JSONValue — passed through unchanged

Every entry below is encoded with toJSON and decoded back with fromJSON, and the two agree — the mapping round-trips:

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "wired_showcase"
    dependency "sparkles:wired" version="*"
+/
import std.stdio : writefln;
import std.sumtype : SumType;
import std.typecons : Nullable, Ternary;
import sparkles.wired : fromJSON, toJSON;

enum Suit
{
    spades,
    hearts,
}

struct Card
{
    Suit suit;
    int rank;
}

alias Cell = SumType!(int, string);

void show(T)(string label, T value)
{
    auto json = value.toJSON.value;     // encode → JSONValue
    auto back = json.fromJSON!T.value;  // decode again → T
    writefln("%-12s %-28s round-trips=%s", label, json.toString, back == value);
}

void main()
{
    show("int",         42);
    show("double",      3.5);
    show("bool",        true);
    show("string",      "hi");
    show("enum",        Suit.hearts);
    show("enum[]",      [Suit.spades, Suit.hearts]);
    show("int[string]", ["a": 1, "b": 2]);
    show("int[Suit]",   [Suit.spades: 1, Suit.hearts: 2]);
    show("struct",      Card(Suit.hearts, 10));
    show("SumType",     Cell("text"));
    show("Nullable",    Nullable!int(7));
    show("Ternary",     Ternary.unknown);
}
int          42                           round-trips=true
double       3.5                          round-trips=true
bool         true                         round-trips=true
string       "hi"                         round-trips=true
enum         "hearts"                     round-trips=true
enum[]       ["spades","hearts"]          round-trips=true
int[string]  {"a":1,"b":2}                round-trips=true
int[Suit]    {"hearts":2,"spades":1}      round-trips=true
struct       {"rank":10,"suit":"hearts"}  round-trips=true
SumType      "text"                       round-trips=true
Nullable     7                            round-trips=true
Ternary      null                         round-trips=true

Enum wire names — @WireName

By default an enum member maps to its source name. Annotate it with @WireName to decouple the JSON spelling from the D identifier — useful for kebab-case or otherwise non-identifier wire names. (For a whole-enum recasing rule, reach for @WireCase instead.) Both directions honour the override:

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "wired_enum_names"
    dependency "sparkles:wired" version="*"
+/
import std.json : parseJSON;
import std.stdio : writeln;
import sparkles.wired : fromJSON, toJSON, WireName;

enum Level
{
    @WireName("low") low,
    @WireName("high-priority") high,
}

void main()
{
    writeln(Level.high.toJSON.value);                                  // custom wire name
    writeln(parseJSON(`"high-priority"`).fromJSON!Level.value == Level.high);
}
"high-priority"
true