Skip to content

Instantly share code, notes, and snippets.

@dselman
Created March 19, 2024 21:49
Show Gist options
  • Save dselman/67d031701af22b80fba3e55b3da6dbf0 to your computer and use it in GitHub Desktop.
Save dselman/67d031701af22b80fba3e55b3da6dbf0 to your computer and use it in GitHub Desktop.
Zig: Heterogenous array JSON serialisation
{
"animals" : [
{
"class" : "Cat",
"name" : "Tabby"
},
{
"class" : "Cat",
"name" : "Tiddles",
"ferocity" : 10
},
{
"class" : "Dog",
"name" : "Fido",
"dogBreed" : "Spaniel"
}
]
}
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
pub fn parseStructBody(
comptime T: type,
allocator: Allocator,
source: anytype,
options: std.json.ParseOptions,
) std.json.ParseError(@TypeOf(source.*))!T {
switch (@typeInfo(T)) {
.Struct => |structInfo| {
var r: T = undefined;
var fields_seen = [_]bool{false} ** structInfo.fields.len;
while (true) {
var name_token: ?std.json.Token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?);
const field_name = switch (name_token.?) {
inline .string, .allocated_string => |slice| slice,
.object_end => { // No more fields.
break;
},
else => {
return error.UnexpectedToken;
},
};
inline for (structInfo.fields, 0..) |field, i| {
if (field.is_comptime) @compileError("comptime fields are not supported: " ++ @typeName(T) ++ "." ++ field.name);
if (std.mem.eql(u8, field.name, field_name)) {
// Free the name token now in case we're using an allocator that optimizes freeing the last allocated object.
// (Recursing into innerParse() might trigger more allocations.)
freeAllocated(allocator, name_token.?);
name_token = null;
if (fields_seen[i]) {
switch (options.duplicate_field_behavior) {
.use_first => {
// Parse and ignore the redundant value.
// We don't want to skip the value, because we want type checking.
_ = try std.json.innerParse(field.type, allocator, source, options);
break;
},
.@"error" => return error.DuplicateField,
.use_last => {},
}
}
// std.debug.print("Setting field: {s}\n", .{field.name});
@field(r, field.name) = try std.json.innerParse(field.type, allocator, source, options);
fields_seen[i] = true;
break;
}
} else {
// Didn't match anything.
freeAllocated(allocator, name_token.?);
if (options.ignore_unknown_fields) {
try source.skipValue();
} else {
return error.UnknownField;
}
}
}
try fillDefaultStructValues(T, &r, &fields_seen);
return r;
},
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
}
unreachable;
}
fn freeAllocated(allocator: Allocator, token: std.json.Token) void {
switch (token) {
.allocated_number, .allocated_string => |slice| {
allocator.free(slice);
},
else => {},
}
}
fn fillDefaultStructValues(comptime T: type, r: *T, fields_seen: *[@typeInfo(T).Struct.fields.len]bool) !void {
inline for (@typeInfo(T).Struct.fields, 0..) |field, i| {
if (!fields_seen[i]) {
if (field.default_value) |default_ptr| {
const default = @as(*align(1) const field.type, @ptrCast(default_ptr)).*;
@field(r, field.name) = default;
} else {
std.debug.print("Missing field: {s}\n", .{field.name});
return error.MissingField;
}
}
}
}
const std = @import("std");
const testing = std.testing;
const ArrayList = @import("std").ArrayList;
const Allocator = std.mem.Allocator;
const StructParser = @import("./structparser.zig");
const DogBreed = enum {
Spaniel,
Poodle,
JackRussel
};
const Cat = struct {
name: []const u8,
ferocity: ?u32=0,
};
const Dog = struct {
name: []const u8,
dogBreed: DogBreed,
};
const Animal = union(enum) {
Dog: Dog,
Cat: Cat,
pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
if (.object_begin != try source.next()) return error.UnexpectedToken;
// key
_ = try source.nextAlloc(allocator, options.allocate.?);
// class value
const classNameToken = try source.nextAlloc(allocator, options.allocate.?);
const className = switch (classNameToken) {
inline .string, .allocated_string => |k| k,
else => unreachable,
};
// std.debug.print("class: {s}\n", .{className});
const classType = std.meta.stringToEnum(std.meta.Tag(@This()), className) orelse return error.UnexpectedToken;
const result = switch(classType) {
.Cat => {
return Animal {
.Cat = try StructParser.parseStructBody(Cat, allocator, source, options),
};
},
.Dog => {
return Animal {
.Dog = try StructParser.parseStructBody(Dog, allocator, source, options),
};
},
};
if (.object_end != try source.next()) return error.UnexpectedToken;
return result;
}
};
const PetCollection = struct {
animals: []Animal,
};
test "create pets from file" {
const file_name = "./src/animals.json";
const data = try std.fs.cwd().readFileAlloc(testing.allocator, file_name, std.math.maxInt(usize));
// std.debug.print("{s}\n", .{data});
defer testing.allocator.free(data);
const parsed = try std.json.parseFromSlice(PetCollection, testing.allocator, data,
.{
.ignore_unknown_fields = false
});
defer parsed.deinit();
var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
try std.json.stringify(parsed.value, .{}, string.writer());
std.debug.print("{s}\n", .{string.items});
const result = parsed.value;
switch(result.animals[0]) {
.Cat => |cat| {
const are_equal = std.mem.eql(u8, cat.name, "Tabby");
try testing.expect(are_equal);
},
else => unreachable,
}
}
@dselman
Copy link
Author

dselman commented Mar 20, 2024

Improved...

const std = @import("std");
const testing = std.testing;
const ArrayList = @import("std").ArrayList;
const Allocator = std.mem.Allocator;

const DogBreed = enum {
	Spaniel,
	Poodle,
	JackRussel
};

const Cat = struct {
    name: []const u8,
    ferocity: ?i64=0,
};

const Dog = struct {
    name: []const u8,
    dogBreed: DogBreed,
};

const Animal = union(enum) {
    Dog: Dog,
    Cat: Cat,
    pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
        const parsed = try std.json.innerParse(std.json.Value, allocator, source, options);
        const class = parsed.object.get("$class") orelse return error.UnexpectedToken;
        const classType = std.meta.stringToEnum(std.meta.Tag(@This()), class.string) orelse return error.UnexpectedToken;
        const result = switch(classType) {
            .Cat => {
                const name = parsed.object.get("name") orelse return error.UnexpectedToken;
                const ferocity = if(parsed.object.get("ferocity")) |v| v.integer else null;
                return Animal {
                    .Cat = Cat {
                        .name = name.string,
                        .ferocity = ferocity
                    }
                };
            },
            .Dog => {
                const name = parsed.object.get("name") orelse return error.UnexpectedToken;
                const dogBreed = parsed.object.get("dogBreed") orelse return error.UnexpectedToken;
                const dogBreedType = std.meta.stringToEnum(DogBreed, dogBreed.string) orelse return error.UnexpectedToken;
                return Animal {
                    .Dog = Dog {
                        .name = name.string,
                        .dogBreed = dogBreedType
                    }
                };
            },
        };
        if (.object_end != try source.next()) return error.UnexpectedToken;
        return result;
    }
};

const PetCollection = struct {
       animals: []Animal,
};

test "create pets from file" {
    const file_name = "./src/animals.json";
    const data = try std.fs.cwd().readFileAlloc(testing.allocator, file_name, std.math.maxInt(usize));
    // std.debug.print("{s}\n", .{data});
    defer testing.allocator.free(data);
    const parsed = try std.json.parseFromSlice(PetCollection, testing.allocator, data, 
    .{
        .ignore_unknown_fields = false
    });
    defer parsed.deinit();

    var buf: [1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buf);
    var string = std.ArrayList(u8).init(fba.allocator());
    try std.json.stringify(parsed.value, .{}, string.writer());
    std.debug.print("{s}\n", .{string.items});

    const result = parsed.value;
    switch(result.animals[0]) {
        .Cat => |cat| {
            const are_equal = std.mem.eql(u8, cat.name, "Tabby");
            try testing.expect(are_equal);
        },
        else => unreachable,
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment