Handling real-world JSON documents with Zig v0.11
JSON module in Zig's standard library is quirky and difficult to use due to lack of documentation. It also defaults to unintuitive object structure, probably because of language design and performance reasons.
Here are how I worked around it to parse/serialize JSON documents for my personal project.
Tagged union
const T = union(enum) {
string: []const u8,
number: i64,
};
Given the above tagged union, Zig v0.10 parses from and serializes to an intuitive JSON value.
"Foo"
100
However, Zig v0.11 radically changed std.json
. The above T
now parsed from and serializes to:
{ "string": "Foo" }
{ "number": 100 }
In order to parse from and serialize to the previous "normal" JSON value, you have to implement those method to your tagged union: jsonParse
, jsonParseFromValue
and jsonStringify
.
const T = union(enum) {
string: []const u8,
number: i64,
pub fn jsonStringify(self: *const @This(), jw: anytype) !void {
try switch (self.*) {
.string => |str| jw.write(str),
.number => |num| jw.write(num),
};
}
pub fn jsonParse(
_: mem.Allocator,
source: anytype,
_: json.ParseOptions,
) json.ParseError(@TypeOf(source.*))!@This() {
const value = try json.innerParse(json.Value, allocator, source, options);
return @This().jsonParseFromValue(allocator, value, options);
}
// You can write this branching directly inside `jsonParse`, unless
// this structure is indirectly parsed
pub fn jsonParseFromValue(
allocator: mem.Allocator,
value: json.Value,
options: json.ParseOptions,
) json.ParseFromValueError!@This() {
return switch (value) {
.string => |v| @This(){ .string = v },
.integer => |v| @This(){ .number = v },
else => error.UnexpectedToken,
};
}
};
json.Value
is like AST node of a JSON document.
While I'm not fan of the default data schema, writing custom parsing/serializing method is quite intuitive and fun.
Optional fields
You can parse JSON object with an optional field using default value and null
.
const User = struct {
name: []const u8,
email: ?[]const u8 = null,
};
{ "name": "Alice", "email": "alice@example.com" } // OK
{ "name": "Bob" } // OK
However, you can't serialize the struct to JSON object with an optional field, because the field (email
in the above example) has a value null
.
{ "name": "Bob", "email": null }
In order to omit an empty field, you have to implement jsonStringify
method to your struct.
const User = struct {
name: []const u8,
email: ?[]const u8 = null,
pub fn jsonStringify(self: *const @This(), jw: anytype) !void {
try jw.beginObject();
try jw.objectField("name");
try jw.write(self.name);
if (self.email) |email| {
try jw.objectField("email");
try jw.write(email);
}
try jw.endObject();
}
};
Although this is error-prone because you have to handle lexical tokens rather than AST nodes, you can fine-tune serialization format.
These are also useful if you have to deal with shitty JSON data, for example ones abusing 1 | 0
as a boolean value.