# json > written against zig 0.15; value serialization section checked against 0.16. building and parsing json. `std.json.Stringify` for output and `std.json.parse` for input. ## building json: describe the value, don't drive the writer `Stringify.write(v)` serializes any value: a struct becomes an object (fields in declaration order), a **tuple becomes an array** (so heterogeneous json arrays like `["vector", "ANN", [0.1, …]]` are just `.{ "vector", "ANN", vec }`), an optional becomes `null`, `[]const u8` becomes a string, and nested anonymous structs nest. `Stringify.valueAlloc(gpa, v, .{})` does the whole body in one call: ```zig const body = try std.json.Stringify.valueAlloc(alloc, .{ .rank_by = .{ "Max", .{ .{ "name", "BM25", text }, .{ "caption", "BM25", text } } }, .top_k = top_k, .include_attributes = .{ "name", "url" }, }, .{}); defer alloc.free(body); ``` that replaced 83 `jw.beginObject/objectField/write/endArray` lines across four modules of a search backend with four value literals — same bytes out, and the shape of the request is now readable as a shape. reach for the incremental api below only when the value can't be expressed as a literal: streaming a large array, or embedding raw pre-serialized json. `Options` defaults: `.minified`, `emit_null_optional_fields = true`. ## building json incrementally use `json.Stringify` with a writer. call methods to build the structure incrementally: ```zig 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: ```zig 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](https://tangled.sh/@zzstoatzz.io/coral/tree/main/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: ```zig 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: ```zig // 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](https://tangled.sh/zzstoatzz.io/logfire-zig/tree/main/src/exporter.zig) ## sources - coral — backend/src/entities.zig - logfire-zig — src/exporter.zig