Skip to content

json

nate
Jul 8, 20262 min read1 read

json

ritten against zig 0.15.

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

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions