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.
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");
}
$ 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()
.
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);
}
$ 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
.
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));
}
$ 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.
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);
}
$ 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:
{
"url": null,
"width": 800,
"height": 600,
"bit_depth": 8,
"url": "https://example.org"
}
And the following data structure:
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:
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:
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");
}
$ 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:
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);
}
$ zig test json.zig
All 1 tests passed.
$ █
Validate JSON
Validating a JSON string is very simple.
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);
}
$ zig test json.zig
All 1 tests passed.
$ █