Announcing Getty 0.3.0
I am happy to announce a new version of Getty, 0.3.0.
Getty is a framework for building robust, optimal, and reusable serializers/deserializers in Zig. To install Getty, follow the instructions listed on the Installation page.
What's in 0.3.0
Deserialize anything
The deserializeAny
method has been added to the getty.Deserializer
interface.
deserializeAny
enables deserializers of self-describing formats (e.g., JSON)
to drive themselves based on their own input data. This can be useful if, for
instance, a user wants to deserialize into a value from multiple possible data
types (e.g., JSON array or JSON object).
const std = @import("std");
const getty = @import("getty");
const json = @import("json");
const Allocator = std.mem.Allocator;
const Point = struct {
x: i32,
y: i32,
pub const @"getty.db" = struct {
// 👋 Here, we call deserializeAny to let the deserializer drive
// itself.
pub fn deserialize(a: ?Allocator, comptime _: type, d: anytype, v: anytype) !Point {
return try d.deserializeAny(a, v);
}
// 👋 Here, we define a visitor that is able to produce Point values
// from both Maps and Sequences.
pub fn Visitor(comptime _: type) type {
return struct {
pub usingnamespace getty.de.Visitor(
@This(),
Point,
.{
.visitMap = visitMap,
.visitSeq = visitSeq,
},
);
pub fn visitMap(_: @This(), a: ?Allocator, comptime _: type, map: anytype) !Point {
var point: Point = undefined;
while (try map.nextKey(a, []const u8)) |key| {
if (std.mem.eql(u8, "x", key)) {
point.x = try map.nextValue(a, i32);
continue;
}
if (std.mem.eql(u8, "y", key)) {
point.y = try map.nextValue(a, i32);
continue;
}
return error.UnknownField;
}
return point;
}
pub fn visitSeq(_: @This(), a: ?Allocator, comptime _: type, seq: anytype) !Point {
var point: Point = undefined;
inline for (std.meta.fields(Point)) |field| {
if (try seq.nextElement(a, i32)) |elem| {
@field(point, field.name) = elem;
}
}
if (try seq.nextElement(a, getty.de.Ignored) != null) {
return error.InvalidLength;
}
return point;
}
};
}
};
};
pub fn main() !void {
// 👋 Here, we deserialize into a Point from a JSON object.
const str1 = "{\"x\":1,\"y\":2}";
const point1 = try json.fromSlice(std.heap.page_allocator, Point, str1);
// 👋 Here, we deserialize into a Point from a JSON array.
const str2 = "[1,2]";
const point2 = try json.fromSlice(null, Point, str2);
std.debug.print("{}\n", .{point1});
std.debug.print("{}\n", .{point2});
}
New union attributes
The skip
and rename
attributes are now supported by unions.
-
Renaming a union variant will change the tag that Getty uses during (de)serialization.
Zig codeconst std = @import("std"); const json = @import("json"); const allocator = std.heap.page_allocator; const Union = union(enum) { foo: i32, bar: i32, pub const @"getty.sb" = block; pub const @"getty.db" = block; const block = struct { pub const attributes = .{ .foo = .{ .rename = "FOO" }, }; }; }; pub fn main() !void { const s = try json.toSlice(allocator, Union{ .foo = 1 }); defer allocator.free(s); const d = try json.fromSlice(null, Union, s); std.debug.print("{s}\n", .{s}); std.debug.print("{}\n", .{d}); }
-
Attempting to (de)serialize a skipped union variant will return an
UnknownVariant
error.Zig codeconst std = @import("std"); const json = @import("json"); const allocator = std.heap.page_allocator; const Union = union(enum) { foo: i32, bar: i32, pub const @"getty.sb" = struct { pub const attributes = .{ .bar = .{ .skip = true }, }; }; }; pub fn main() !void { _ = try json.toSlice(allocator, Union{ .bar = 1 }); }
Goodbye is
function (sort of)
Type-defined blocks no longer require an is
function to be declared.
Type-defined blocks are only ever processed for the type that they're defined
in, so it didn't really make any sense to require an is
function in them.
Also gone are type-defined tuples, for pretty much the same reason. If type-defined blocks are only ever processed against whatever type they're defined in, there's no reason to allow multiple blocks to be specified in a tuple since the first matching block will always get chosen.
const std = @import("std");
const json = @import("json");
const allocator = std.heap.page_allocator;
const Point = struct {
x: i32,
y: i32,
pub const @"getty.sb" = struct {
pub fn serialize(v: anytype, ser: anytype) !@TypeOf(ser).Ok {
var s = try ser.serializeSeq(2);
const seq = s.seq();
try seq.serializeElement(v.x);
try seq.serializeElement(v.y);
return try seq.end();
}
};
};
pub fn main() !void {
const point = Point{ .x = 1, .y = 2 };
const slice = try json.toSlice(allocator, point);
defer allocator.free(slice);
std.debug.print("{s}\n", .{slice});
}
Key allocation
Deserialization of Map keys has always been a pain point for Getty.
Specifically, it was difficult for a visitor to tell whether or not a
deserialized key was allocated, and therefore whether or not it should/could be
deallocated. Which is important, since knowing the difference would allow
us to avoid memory leaks, enable compile-time deserialization of struct
values, and unlock performance gains for certain map-like data structures.
Originally, the convention was to simply assume that all deserialized (pointer)
map keys are allocated, except in the case of struct
s. However, that turned
out to be super inconsistent, confusing for newcomers, and it didn't even
really work in the general case.
Getty 0.3.0 fixes this problem by adding a new method to the
getty.de.MapAccess
interface, isKeyAllocated
. Visitors can simply query
this method to determine whether or not a key is allocated. By default, the
method returns true
if the key type being deserialized into is a pointer.
But of course, implementations are free to override this method however they
like!
Below is a snippet from Getty JSON's
getty.de.MapAccess
implementation for struct
values. Here, isKeyAllocated
returns true
only for strings that contain escaped characters.
fn StructAccess(comptime D: type) type {
return struct {
d: *D,
is_key_allocated: bool = false, // 👋 By default, keys are not
// allocated.
const Self = @This();
fn isKeyAllocated(self: *Self, comptime _: type) bool {
return self.is_key_allocated;
}
fn nextKeySeed(self: *Self, a: ?Allocator, seed: anytype) Error!?@TypeOf(seed).Value {
// 👋 Parse token from input data.
if (try self.d.tokens.next()) |token| {
switch (token) {
.String => |t| {
// 👋 Get string from token.
const slice = t.slice(self.d.tokens.slice, self.d.tokens.i - 1);
// 👋 If the string has escaped characters, set
// is_key_allocated to true since unescaping
// a string requires allocation.
self.is_key_allocated = t.escapes == .Some;
// 👋 Give the visitor the correct string.
return switch (t.escapes) {
.None => slice,
.Some => try unescapeString(a.?, token.String, slice),
};
},
.ObjectEnd => return null,
else => {},
}
}
return error.InvalidType;
}
// (cut)
};
}
// (cut)
Ignoring values
Getty's default deserialization tuple now contains a block for getty.de.Ignored
.
By deserializing into this type, visitors can easily skip and ignore
deserialized values, which is useful when deserializing aggregate types. For
example, you often have to do a final call to nextElement
or nextKey
to
make sure that there are no more remaining elements/entries; getty.de.Ignored
is the perfect type to pass in for that final call.
Mandatory errors
To ensure compatibility with Getty's default blocks and to enable visitors to
return error.Unsupported
(which is needed for deserializeAny
), the error
sets of serializers and deserializers must now contain getty.ser.Error
and
getty.de.Error
, respectively.
Getty will check for these error sets at compile-time and let you know if you forgot them.
Support
Some new types are now supported by Getty!
Serialization
Deserialization
- Non-string, sentinel-terminated slices
std.ArrayHashMap
std.ArrayHashMapUnmanaged
std.BoundedArray
std.PackedIntArray
std.PackedIntSlice
std.net.Address
Other Changes
There are other changes in the Getty 0.3.0 release. You can see the full changelog here.