Deserializing tagged JSON objects with metaprogramming

Ok, I have a working solution now, thank you all for your help!
The functions look like this:

fn makeJsonParse(comptime T: type) fn (std.mem.Allocator, *std.json.Scanner, std.json.ParseOptions) std.json.ParseError(std.json.Scanner)!T {
    return struct {
        fn jsonParse(allocator: std.mem.Allocator, source: *std.json.Scanner, options: std.json.ParseOptions) std.json.ParseError(std.json.Scanner)!T {
            const parsed = try std.json.innerParse(std.json.Value, allocator, source, options);

            if (parsed != .object) {
                return error.UnexpectedToken;
            }
            return T.jsonParseFromValue(allocator, parsed, options);
        }
    }.jsonParse;
}

fn makeJsonParseFromValue(comptime T: type, comptime discriminator: []const u8) fn (std.mem.Allocator, std.json.Value, std.json.ParseOptions) std.json.ParseFromValueError!T {
    switch (@typeInfo(T)) {
        .Union => {},
        else => @compileError("Type must be a union"),
    }
    return struct {
        pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) std.json.ParseFromValueError!T {
            if (source.object.get(discriminator)) |t| {
                var opts = options;
                opts.ignore_unknown_fields = true;
                inline for (@typeInfo(T).Union.fields) |u_field| {
                    if (std.mem.eql(u8, t.string, u_field.name)) {
                        const p = try std.json.parseFromValue(u_field.type, allocator, source, opts);
                        const ret = @unionInit(T, u_field.name, p.value);
                        return ret;
                    }
                }
            }
            return error.MissingField;
        }
    }.jsonParseFromValue;
}

// Example usage
const Field = union(enum) {
    boolean: Boolean,
    integer: Integer,

    pub const jsonParse = makeJsonParse(@This());
    pub const jsonParseFromValue = makeJsonParseFromValue(@This(), "type");
};

const Boolean = struct {
    pub const @"type": []const u8 = "boolean";
    description: ?[]const u8 = null,
    default: ?bool = null,
    @"const": ?bool = null,
};

const Integer = struct {
    pub const @"type": []const u8 = "integer";
    description: ?[]const u8 = null,
    minimum: ?i64 = null,
    maximum: ?i64 = null,
    @"enum": ?[]i64 = null,
    default: ?i64 = null,
    @"const": ?i64 = null,
};

Which is more or less a generic version of @Travis’s solution. I learned a lot about how Zig works!

I think possible improvements would be to add a compile time check to make sure the union field types actually have the field as a declaration but I’ll leave that for later. If anybody has any other suggestions please tell me.

2 Likes