Zig v0.11 での JSON の取り扱い
Zig で JSON ドキュメントをパース・シリアライズしようとしてハマったのでメモ。
std.json
はソースコードドキュメントもテストもろくにないため Issue なんかを調べる羽目になった。
プリミティブのパース・シリアライズは問題ないが、オブジェクト周りに関してははっきり言ってデザインがよろしくない。 現実的な JSON ドキュメント、特に既存のスキーマを扱う場合は以下のような追加実装が必ず必要になってくるだろう。
Tagged Union
const T = union(enum) {
string: []const u8,
number: i64,
};
v0.10 では上記のコードの T
は文字列もしくは数値の JSON ドキュメントに対応する。
"Foo" // OK
100 // OK
true // NG
しかし v0.11 で union
のパース・シリアライズが大きく変更され、 { "<タグ名>": データ }
という JSON ドキュメントにしなくてはならない。
{ "string": "Foo" } // OK
{ "number": 100 } // OK
"Foo" // NG
100 // NG
v0.10 で扱えていた一般的なユニオン型表現に対応するためには pub fn jsonParse
と pub fn jsonParseFromValue
、 pub fn jsonStringify
を対象の union
(上記の例だと T
) に実装しなくてはならない。
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);
}
// 間接的に呼ばれない場合は jsonParse に全部書いちゃってもいい
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,
};
}
};
イメージとしては AST のノードを受け取って許容できれば実体を返し、できなければエラーを返す、といった感じ。
任意フィールド
値がない場合に null
ではなくフィールド自体がない場合、構造体フィールドに初期値を指定すればパースはされる。
const User = struct {
name: []const u8,
email: ?[]const u8 = null,
};
{ "name": "Alice", "email": "alice@example.com" } // OK
{ "name": "Bob" } // OK
しかしこのままシリアライズするとフィールドに null
がセットされてしまう。
{ "name": "Bob", "email": null }
構造体フィールドの値が null
の場合に JSON のフィールドを省略したい場合は pub fn jsonStringify
を構造体に実装する必要がある。
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();
}
};
かなりプリミティブな内容を書くことになるが、出力される JSON を細かくチューニングできるとも言えるだろう。 AST を扱うパース処理と近いが、こちらはどちらかというとトークンを手続き的に積み上げていくイメージだろう。
任意フィールドや Tagged Union だけでなく、例えば boolean を 1 | 0
で表現するような 💩 JSON ドキュメントを扱う際にもこれらは役に立つ。