json

json

building and parsing json. zig 0.15 has std.json.Stringify for output and std.json.parse for input.

building json

use json.Stringify with a writer. call methods to build the structure incrementally:

const std = @import("std");
const json = std.json;

fn buildJson(allocator: std.mem.Allocator, data: MyData) ![]u8 {
    var output: std.Io.Writer.Allocating = .init(allocator);
    errdefer output.deinit();
    var jw: json.Stringify = .{ .writer = &output.writer };

    try jw.beginObject();
    try jw.objectField("name");
    try jw.write(data.name);           // strings, ints, floats, bools
    try jw.objectField("count");
    try jw.write(data.count);
    try jw.objectField("items");
    try jw.beginArray();
    for (data.items) |item| {
        try jw.write(item);
    }
    try jw.endArray();
    try jw.endObject();

    return output.toOwnedSlice();
}

key methods:

  • beginObject() / endObject() - { and }
  • beginArray() / endArray() - [ and ]
  • objectField("key") - write a key, call write() next for the value
  • write(value) - writes any json-serializable value (handles UTF-8 and escaping correctly)

fixed buffer (no allocation)

when you have a stack buffer and don't want to allocate:

fn toJson(data: MyData, buf: []u8) []const u8 {
    var w: std.Io.Writer = .fixed(buf);
    var jw: std.json.Stringify = .{ .writer = &w };

    jw.beginObject() catch return "{}";
    jw.objectField("name") catch return "{}";
    jw.write(data.name) catch return "{}";
    jw.endObject() catch return "{}";

    return w.buffered();  // slice of what was written
}

key difference: use std.Io.Writer = .fixed(buf) and call w.buffered() to get the written slice.

see: coral/backend/src/entities.zig

raw json passthrough

when you have a json string that's already valid and want to embed it without re-parsing:

try jw.objectField("nested");
try jw.beginWriteRaw();
try jw.writer.writeAll(raw_json_string);
jw.endWriteRaw();

useful when proxying json from external apis.

gotcha: numbers vs strings

otlp and other protocols care about the difference. timestamps are often strings (to avoid precision loss with large nanosecond values), but counts are numbers:

// timestamp as string
try jw.objectField("timeUnixNano");
var buf: [32]u8 = undefined;
const s = std.fmt.bufPrint(&buf, "{d}", .{timestamp_ns}) catch unreachable;
try jw.write(s);  // "1234567890000000000"

// count as number
try jw.objectField("count");
try jw.write(count);  // 42

see: logfire-zig/exporter.zig