Recipe: A Custom JSON Serializer
The problem
The default JSON serializer emits your struct exactly as declared: same field
names, same values, nothing more. Often the wire format has to differ. The
field is is_admin in Zig but admin in the API, or the response needs a
value you compute rather than store. You want that control without hand-rolling
string concatenation and without giving up the checks that keep the output
valid.
Zig’s answer is a hook: declare pub fn jsonStringify on the type and
std.json.Stringify calls it instead of walking the fields.
The plan
- Define the struct as usual.
- Add
pub fn jsonStringify(self: T, jws: anytype) !void.jwsis a pointer to the write stream. - Drive the stream:
beginObject, thenobjectField(name)andwrite(value)per field, thenendObject. - Serialize with
std.json.Stringify.value(x, .{}, writer). The hook is picked up automatically.
const std = @import("std");
// Identical fields, no hook: this is what the default serializer emits.
const Plain = struct {
id: u64,
name: []const u8,
is_admin: bool,
created_at: i64,
};
const User = struct {
id: u64,
name: []const u8,
is_admin: bool,
created_at: i64,
// When present, std.json.Stringify calls this instead of walking the
// fields. `jws` is a *std.json.Stringify write stream. It balances
// begin/end and quotes strings for you, so the output is always valid.
pub fn jsonStringify(self: User, jws: anytype) !void {
try jws.beginObject();
try jws.objectField("user_id"); // rename id
try jws.write(self.id);
try jws.objectField("full_name"); // rename name
try jws.write(self.name);
try jws.objectField("admin"); // rename is_admin
try jws.write(self.is_admin);
// A computed field the struct does not store: whole days since epoch.
try jws.objectField("epoch_day");
try jws.write(@divTrunc(self.created_at, std.time.s_per_day));
try jws.endObject();
}
};
pub fn main(init: std.process.Init) !void {
var buf: [1024]u8 = undefined;
var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buf);
const out = &file_writer.interface;
const id = 101;
const name = "Ziggy Stardust";
const admin = true;
const ts = 1672531200; // 2023-01-01T00:00:00Z
try out.writeAll("default: ");
try std.json.Stringify.value(
Plain{ .id = id, .name = name, .is_admin = admin, .created_at = ts },
.{},
out,
);
try out.writeAll("\n");
try out.writeAll("custom: ");
try std.json.Stringify.value(
User{ .id = id, .name = name, .is_admin = admin, .created_at = ts },
.{},
out,
);
try out.writeAll("\n");
try out.flush();
}The hook, field by field
objectField("user_id") writes the key, and write(self.id) writes the
value with normal serialization, including quoting strings and escaping them.
Because you name each key yourself, renaming is just typing a different
string: is_admin becomes admin, name becomes full_name. The stream
tracks structure, so if you forget an endObject or write a value where a key
belongs, it is a compile-time or runtime error rather than silently malformed
JSON.
Computed fields
epoch_day is not a struct field. The hook computes it from created_at and
writes it like any other value. This is the main reason to write a custom
serializer: the output can carry derived data (a formatted timestamp, a
signed URL, a total) that you would not want to store on the struct itself.
write versus print
write(value) serializes a Zig value as JSON: numbers stay numbers, strings
get quoted. When you need to emit a literal fragment, jws.print("{d}", .{n})
writes raw text into the stream, so you are responsible for the quotes and
escaping. Prefer write unless you specifically need the raw form; it is the
one that cannot produce invalid output.
Variations
- Whitespace:
std.json.Stringify.value(x, .{ .whitespace = .indent_2 }, w)pretty-prints with two-space indentation. - Omitting fields: a hook can skip a field entirely (write nothing for it), which the default serializer cannot do.
- Unions and enums: the same
jsonStringifyhook works on tagged unions and enums, not just structs. - The read side: parsing back is covered by the JSON config recipe, which validates the shape as it loads.