Skip to content

Parsing JSON in Zig

Posted on:2024-07-26 | 4 min read

As I was building my Waybar Wise FX Rate module, I found out the Zig JSON docs could have been better for newcomers to the language, such as myself. I couldn’t figure out how to parse an array of objects. But it was great when I asked the question in Zig’s Discord, and people helped me!

Table of contents

Open Table of contents

Intro

JSON in Zig works differently from other popular programming languages. You have the option to decode the JSON into primitives with json.Value, or decode directly to a Struct.

When working with primitives, the decoded values are inside the type json.Value. This type has different fields to access each key value depending on the underlying object (See docs). For example, having this parsed JSON:

{ "id": 125, "name": "real name" }

You can manipulate it in the following way:

// Access the root object.
parsed.value.object;

// Access the ID as `i64`. The field `object` is a StringArrayHashMap(Value); the `get` function returns `?Value`.
parsed.value.object.get("id").?.integer;

// Access the name as `[]const u8`.
parsed.value.object.get("name").?.string;

Working with arrays works in similarly. To iterate over an array, you do the following:

for (parsed.value.array.items) |item| { ... };

Now let’s see proper examples using different techniques + how to decode into structs.

Parsing JSON into a Zig primitive

The following code parses a JSON object and retrieves the "id" value.

test "parse object.id into an ?i64" {
    const allocator = std.testing.allocator;

    const json_str =
        \\ {
        \\   "id": 125,
        \\   "name": "real name"
        \\ }
    ;

    const parsed = try json.parseFromSlice(json.Value, allocator, json_str, .{});
    defer parsed.deinit();

    const id: ?i64 = parsed.value.object.get("id").?.integer;

    try testing.expectEqual(125, id);
}

The type json.Value holds the parsed string and can represent any JSON value:

To get the value, you must to access one of the exposed fields from the type. For example, to get the parsed "id" integer, I had to call .integer.

Parsing a JSON Array of objects with ID to an integer ArrayList

Going from this [ { "id": 125 }, { "id": 126 } ] to [125, 126].

test "parse array ids into an ArrayList" {
    const allocator = std.testing.allocator;

    const json_str =
        \\ [
        \\   { "id": 125 },
        \\   { "id": 126 }
        \\ ]
    ;

    const parsed = try json.parseFromSlice(json.Value, allocator, json_str, .{});
    defer parsed.deinit();

    var ids = std.ArrayList(i64).init(allocator);
    defer ids.deinit();

    for (parsed.value.array.items) |json_object| {
        if (json_object.object.get("id")) |id_value| {
            try ids.append(id_value.integer);
        }
    }

    try testing.expectEqual(125, ids.items[0]);
    try testing.expectEqual(126, ids.items[1]);
}

Parsing JSON Objects into Zig Structs

test "parse array of objects with ids into an Array of Structs" {
    const IdStruct = struct {
        id: i64,
        name: []const u8,
    };

    const allocator = std.testing.allocator;

    const json_str =
        \\ [
        \\   { "id": 125, "name": "Romario" },
        \\   { "id": 126, "name": "Goku" }
        \\ ]
    ;

    const parsed = try json.parseFromSlice([]IdStruct, allocator, json_str, .{});
    defer parsed.deinit();

    try testing.expectEqual(125, parsed.value[0].id);
    try testing.expectEqualStrings("Romario", parsed.value[0].name);

    try testing.expectEqual(126, parsed.value[1].id);
    try testing.expectEqualStrings("Goku", parsed.value[1].name);
}

Caveats

The documentation states that when using an std.heap.ArennaAllocator, you should use json.parseFromSliceLeaky instead. From this excellent post:

In addition to parseFromSlice there’s also a parseFromSliceLeaky. The “leaky” version returns T directly. This version is written assuming that the provided allocator is able to free all allocations, without having to track every individual allocations. It essentially assumes that the provided allocator is something like an ArenaAllocator or a FixedBufferAllocator. In practical terms, parseFromSlice internally creates an ArenaAllocator and returns that allocator with the parsed value whereas parseFromSliceLeaky takes an ArenaAllocator (or something like it) and returns the parsed value. In both cases, you end up with a value of type T tied to an allocator.


A post like this would’ve helped me a ton when starting my journey with Zig. I hope it is helpful to you, too :).

Let me know if you want to parse a specific JSON, and we can figure it out together.