TL;DR: I want to get around that Type.Structs
with decls can’t be reified, or figure out some other way to express my ideas below.
I’m trying to deserialize json objects, where some objects are part of a union that are discriminated by one of their fields, so for example, an integer
object will look like this {"type": "integer", ...}
and a boolean will look like this {"type": "boolean", ...}
, and there is some other type which contain a field what is the union of these. My first attempt at doing this looked something like this
const std = @import("std");
const Int = struct { ty: []const u8 = "integer", max: ?i32, min: ?i32 };
const Bool = struct { ty: []const u8 = "boolean", default: ?bool };
const Field = union(enum) {
int: Int,
bool: Bool,
};
test parse {
const input = \\
\\ { "ty": "integer" }
;
_ = std.json.parseFromSlice(Field, std.testing.allocator, input, .{});
}
This doesn’t work, for afaiu because the json parser expects the json object corresponding to Field
to look like this { "int": { "ty": "integer" } }
. I.e., the current parser expects the discriminator to be key and not part of the object.
I have created a version of field with a custom jsonParse
and jsonParseFromValue
functions, but this is a pattern that will be replicated for more unions, so I wanted to create some helper functions that allow me to do two things:
- Create structs with a const declaration of the discriminant, like this
{ const ty = "integer"; }
, the other fields, and adds customjsonParse
andjsonParseFromValue
to take into account that one of the fields of the json object is now a declaration and not a field. - Create a helper to create unions that will deserialize the unions as explained above (i.e. based on the discriminant field), taking the decl’s name and adds custom
jsonParse
andjsonParseFromValue
functions.
Here I ran into an issue with not being able to reify Type.Struct
s with decls in them, and I don’t see any way to create such helpers without adding decls to Type.Struct
s, which is forbidden by design.
Here's some WIP code that I've tested my idea with.
const std = @import("std");
const testing = std.testing;
const Integer = struct {
pub const @"type": []const u8 = "integer";
min: ?i64 = null,
max: ?i64 = null,
default: ?i64 = null,
pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
const parsed = try std.json.innerParse(std.json.Value, allocator, source, options);
if (parsed != .object) {
return error.UnexpectedToken;
}
return jsonParseFromValue(allocator, parsed, options);
}
pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() {
if (!std.mem.eql(u8, @This().type, source.object.get("type").?.string)) {
return error.MissingField;
}
var v: @This() = undefined;
inline for (@typeInfo(@This()).Struct.fields) |field| {
if (source.object.get(field.name)) |field_name| {
const p_ = try std.json.parseFromValue(field.type, allocator, field_name, options);
@field(v, field.name) = p_.value;
} else if (@typeInfo(field.type) == .Optional) {
@field(v, field.name) = null;
}
}
return v;
}
};
pub fn TaggedJsonStruct(comptime s: type, comptime tag: []const u8) type {
const MethodContainer = struct {
pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
const parsed = try std.json.innerParse(std.json.Value, allocator, source, options);
if (parsed != .object) {
return error.UnexpectedToken;
}
return jsonParseFromValue(allocator, parsed, options);
}
pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() {
if (!std.mem.eql(u8, @This().type, source.object.get("type").?.string)) {
return error.MissingField;
}
var v: @This() = undefined;
inline for (@typeInfo(@This()).Struct.fields) |field| {
if (source.object.get(field.name)) |field_name| {
const p_ = try std.json.parseFromValue(field.type, allocator, field_name, options);
@field(v, field.name) = p_.value;
} else if (@typeInfo(field.type) == .Optional) {
@field(v, field.name) = null;
}
}
return v;
}
};
_ = tag;
var S_Type = @typeInfo(s);
inline for (S_Type.Struct.decls) |decl| {
@compileLog("struct ?= {}", .{decl});
}
S_Type.Struct.decls = S_Type.Struct.decls ++ @typeInfo(MethodContainer).Struct.decls;
@compileLog("??? = {}", .{S_Type});
return @Type(S_Type);
}
test "TaggedJsonStruct" {
const T = TaggedJsonStruct(struct {
const ty = "abc";
}, "ty");
_ = T;
}
const Boolean = struct {
const @"type": []const u8 = "boolean";
default: ?bool = null,
};
const Field = union(enum) {
integer: Integer,
boolean: Boolean,
};
test "integer.json" {
const input =
\\{ "type": "integer", "default": 0}
;
const parsed = try std.json.parseFromSlice(Integer, std.testing.allocator, input, .{});
defer parsed.deinit();
try std.testing.expectEqual(null, parsed.value.max);
try std.testing.expectEqual(0, parsed.value.default);
}
What I need help with is
- Is there any way to salvage my idea using metaprogramming to generate these functions or do I have to build them by hand?
- Is there some part of the
json
library that supports these kinds of discriminated tags that I’ve missed?