Zig w/ Example - JSON

Encode into JSON

Also known as JSON marshalling.

In this example, we will encode a struct into a JSON string and save it into a file named data.json.

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

const Data = struct {
    name: []const u8,
    is_alive: bool,
    age: u8,
    address: struct {
        street_address: []const u8,
        city: []const u8,
        state: []const u8,
        postal_code: []const u8,
    },
    children_age: []const i8,
    spouse: ?[]const u8,
};

test "Encode a struct into a JSON string" {
    const foo = Data{
        .name = "John Smith",
        .is_alive = true,
        .age = 45,
        .address = .{
            .street_address = "21 2nd Street",
            .city = "New York",
            .state = "NY",
            .postal_code = "10021-3100",
        },
        .children_age = &.{ 12, 8, 21 },
        .spouse = null,
    };

    var file = try std.fs.cwd().createFile("data.json", .{});
    defer file.close();

    const options = std.json.StringifyOptions{
        .whitespace = .{
            .indent_level = 0,
            .indent = .{ .Space = 4 },
            .separator = true,
        },
    };

    try std.json.stringify(foo, options, file.writer());

    _ = try file.write("\n");
}
Terminal
$ zig test json.zig
All 1 tests passed.
$ cat data.json
{
    "name": "John Smith",
    "is_alive": true,
    "age": 45,
    "address": {
        "street_address": "21 2nd Street",
        "city": "New York",
        "state": "NY",
        "postal_code": "10021-3100"
    },
    "children_age": [
        12,
        8,
        21
    ],
    "spouse": null
}

$ 

Decode from JSON

Also known as JSON unmarshalling.

Suppose we know about the structure of the JSON data, and that our struct is guaranteed to be able to handle all of the parsed data.

To unmarshal, we use std.json.parse().

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

const Data = struct {
    name: []const u8,
    is_alive: bool,
    age: u8,
    address: struct {
        street_address: []const u8,
        city: []const u8,
        state: []const u8,
        postal_code: []const u8,
    },
    children_age: []const i8,
    spouse: ?[]const u8,
};

test "Decode into a struct from a JSON string" {
    const options = std.json.ParseOptions{
        .allocator = std.testing.allocator,
    };

    // On success, `foo` will be of type `Data` struct.
    const foo = try std.json.parse(Data, &std.json.TokenStream.init(
        \\{
        \\    "name": "John Smith",
        \\    "is_alive": true,
        \\    "age": 45,
        \\    "address": {
        \\        "street_address": "21 2nd Street",
        \\        "city": "New York",
        \\        "state": "NY",
        \\        "postal_code": "10021-3100"
        \\    },
        \\    "children_age": [12, 8, 21],
        \\    "spouse": null
        \\}
    ), options);

    defer std.json.parseFree(Data, foo, options);

    try std.testing.expectEqualStrings(foo.name, "John Smith");
    try std.testing.expectEqual(foo.is_alive, true);
    try std.testing.expectEqual(foo.age, 45);
    try std.testing.expectEqualStrings(foo.address.street_address, "21 2nd Street");
    try std.testing.expectEqualSlices(i8, foo.children_age, &.{ 12, 8, 21 });
    try std.testing.expect(foo.spouse == null);
}
Terminal
$ zig test json.zig
All 1 tests passed.
$ 

Decode: Handling null

Occasionally, a JSON's key type and its struct type will differ, for instance when a key's value contains a null value instead.

The example below will return an error.UnexpectedToken.

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

const Data = struct {
    url: []const u8,
    width: u32,
    height: u32,
};

const json_data =
    \\{
    \\    "url": null,
    \\    "width": 800,
    \\    "height": 600
    \\}
;

test "Invalid JSON type: null -> []const u8" {
    const options = std.json.ParseOptions{
        .allocator = std.testing.allocator,
    };

    try std.testing.expectError(error.UnexpectedToken, std.json.parse(Data, &std.json.TokenStream.init(json_data), options));
}
Terminal
$ zig test json.zig
All 1 tests passed.
$ 

As you can see, Data.url is null instead of a valid string type. Easy solution of this would be to simply turn Data.url type into an optional type.

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

const Data = struct {
    url: ?[]const u8,
    width: u32,
    height: u32,
};

const json_data =
    \\{
    \\    "url": null,
    \\    "width": 800,
    \\    "height": 600
    \\}
;

test "Valid JSON type: null -> ?[]const u8" {
    const options = std.json.ParseOptions{
        .allocator = std.testing.allocator,
    };

    const foo = try std.json.parse(Data, &std.json.TokenStream.init(json_data), options);
    defer std.json.parseFree(Data, foo, options);

    try std.testing.expect(foo.url == null);
}
Terminal
$ zig test json.zig
All 1 tests passed.
$ 

Decode: Handling duplicate/unknown key

std.json.parse() will also return an error if it encounters duplicate or unknown keys.

Suppose we have the following JSON file:

example.json
{
    "url": null,
    "width": 800,
    "height": 600,
    "bit_depth": 8,
    "url": "https://example.org"
}

And the following data structure:

json.zig
const Data = struct {
    url: ?[]const u8,
    width: u32,
    height: u32,
};

To handle this problem, std.json.parse() accepts an optional set of, well, options, that we have to explicitly set.

From the standard library, std.json.ParseOptions{} has the following attributes:

zig/lib/std/json.zig
pub const ParseOptions = struct {
    allocator: ?*Allocator = null,

    /// Behaviour when a duplicate field is encountered.
    duplicate_field_behavior: enum {
        UseFirst,
        Error,
        UseLast,
    } = .Error,

    /// If false, finding an unknown field returns an error.
    ignore_unknown_fields: bool = false,

    allow_trailing_data: bool = false,
};

Thus, we can simply override the default options, like so:

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

const Data = struct {
    url: ?[]const u8,
    width: u32,
    height: u32,
};

const json_data =
    \\{
    \\    "url": null,
    \\    "width": 800,
    \\    "height": 600,
    \\    "bit_depth": 8,
    \\    "url": "https://example.org"
    \\}
;

test "Duplicate and unknown keys" {
    const options = std.json.ParseOptions{
        .allocator = std.testing.allocator,

        .duplicate_field_behavior = .UseLast,
        .ignore_unknown_fields = true,
    };

    const foo = try std.json.parse(Data, &std.json.TokenStream.init(json_data), options);
    defer std.json.parseFree(Data, foo, options);

    try std.testing.expect(foo.url != null);
    try std.testing.expectEqualStrings(foo.url.?, "https://example.org");
}
Terminal
$ zig test json.zig
All 1 tests passed.
$ 

Decode JSON dynamically

In some cases, you don't want to put the decoded JSON into a struct. You simply just want to see whether a key exists or not, and/or find the type of its value.

For that, you can do this:

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

const json_data =
    \\{
    \\    "url": null,
    \\    "width": 800.25,
    \\    "url": "https://example.org",
    \\    "story": {
    \\        "title": "The Adventure of Yada Yada",
    \\        "chapters": 210,
    \\        "scary_parts": [23, 66, 90, 131]
    \\    }
    \\}
;

test "Decode JSON dynamically" {
    var parser = std.json.Parser.init(std.testing.allocator, false);
    defer parser.deinit();

    var tree = try parser.parse(json_data);
    defer tree.deinit();

    const root = tree.root;
    try std.testing.expect(root == .Object);

    const width = root.Object.get("width").?;
    try std.testing.expect(width == .Float);
    try std.testing.expect(width.Float == 800.25);

    // The key `height` doesn't exist, so this variable contains null.
    try std.testing.expect(root.Object.contains("height") == false);
    const height = root.Object.get("height");
    try std.testing.expect(height == null);

    const url = root.Object.get("url").?;

    // When there are duplicate keys, the one appearing last will be the one
    // used.
    //
    // Also, beware that there is a difference between `null` and `.Null`.
    try std.testing.expect(url != .Null);
    try std.testing.expectEqualStrings(url.String, "https://example.org");

    const story = root.Object.get("story").?;
    try std.testing.expect(story == .Object);

    const story_title = story.Object.get("title").?;
    try std.testing.expectEqualStrings(story_title.String, "The Adventure of Yada Yada");

    // Array handling
    const story_scary_parts = story.Object.get("scary_parts").?;
    try std.testing.expect(story_scary_parts.Array.items[0] == .Integer);

    const foo = &[_]i64{ 23, 66, 90, 131 };

    for (story_scary_parts.Array.items) |value, index| {
        try std.testing.expectEqual(value.Integer, foo[index]);
    }

    try std.testing.expect(story.Object.contains("chapters") == true);
    try std.testing.expect(story.Object.contains("ending") == false);
}
Terminal
$ zig test json.zig
All 1 tests passed.
$ 

Validate JSON

Validating a JSON string is very simple.

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

const invalid_json =
    \\{
    \\    "url": null,
    \\}
;

const valid_json =
    \\{
    \\    "url": null
    \\}
;

test "Validate JSON" {
    var is_valid = std.json.validate(invalid_json);
    try std.testing.expect(!is_valid);

    is_valid = std.json.validate(valid_json);
    try std.testing.expect(is_valid);
}
Terminal
$ zig test json.zig
All 1 tests passed.
$