⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

JSON

const std = @import("std");
const expect = std.testing.expect;

const Config = struct {
    name: []const u8,
    port: u16,
    debug: bool = false, // defaults cover missing fields
};

test "parse into a struct" {
    const gpa = std.testing.allocator;
    const text =
        \\{ "name": "server", "port": 8080 }
    ;

    // The parse result owns its allocations; deinit frees them all at once.
    const parsed = try std.json.parseFromSlice(Config, gpa, text, .{});
    defer parsed.deinit();

    try expect(std.mem.eql(u8, parsed.value.name, "server"));
    try expect(parsed.value.port == 8080);
    try expect(parsed.value.debug == false);
}

test "unknown fields are rejected by default" {
    const gpa = std.testing.allocator;
    const text =
        \\{ "name": "x", "port": 1, "extra": true }
    ;
    try std.testing.expectError(
        error.UnknownField,
        std.json.parseFromSlice(Config, gpa, text, .{}),
    );

    // Opt out when the payload may carry fields you do not model.
    const lenient = try std.json.parseFromSlice(Config, gpa, text, .{
        .ignore_unknown_fields = true,
    });
    defer lenient.deinit();
    try expect(lenient.value.port == 1);
}

test "dynamic values when the shape is unknown" {
    const gpa = std.testing.allocator;
    const parsed = try std.json.parseFromSlice(
        std.json.Value,
        gpa,
        \\{ "a": [1, 2, 3] }
    ,
        .{},
    );
    defer parsed.deinit();

    const array = parsed.value.object.get("a").?.array;
    try expect(array.items.len == 3);
    try expect(array.items[0].integer == 1);
}

test "serialise" {
    const gpa = std.testing.allocator;
    var out: std.Io.Writer.Allocating = .init(gpa);
    defer out.deinit();

    try std.json.Stringify.value(
        Config{ .name = "out", .port = 99, .debug = true },
        .{},
        &out.writer,
    );
    try expect(std.mem.indexOf(u8, out.written(), "\"port\":99") != null);
}

Parsing into a struct

parseFromSlice(T, gpa, text, .{}) maps JSON onto your type using its field names. The result owns every allocation it made:

const parsed = try std.json.parseFromSlice(Config, gpa, text, .{});
defer parsed.deinit();
parsed.value.port;

Note parsed.value: the payload lives behind the owner so a single deinit frees the whole tree. Strings in parsed.value point into that arena, so they die with it; duplicate anything you need to outlive the parse.

Missing and unknown fields

A field with a default is optional in the input. An unknown field is an error by default:

error.UnknownField

That default is the right one: it catches typos in config files. Opt out deliberately when consuming a payload you do not fully model:

.{ .ignore_unknown_fields = true }

When the shape is not known

Parse into std.json.Value for a dynamic tree, then walk .object, .array, .integer, .string, and friends. Prefer a concrete struct where you can: you get validation and field names checked at compile time.

Writing

std.json.Stringify.value(x, .{}, &writer) serialises to any writer. Pair it with std.Io.Writer.Allocating to produce a string.