Blocks and Tuples
Blocks are the fundamental building blocks (pun intended) of Getty's (de)serialization process.
They define how types should be serialized or deserialized into. For example,
all of the ways a bool value can be serialized by Getty are specified
in the getty.ser.blocks.Bool
block, and all of the ways that you can deserialize into a [5]i32 are defined in
getty.de.blocks.Array.
Internally, Getty uses blocks to form its core (de)serialization behavior. However, they are also the main mechanism for customization in Getty. Users and (de)serializers can take advantage of blocks in order to customize the way Getty (de)serializes values, as we'll see later on.
Blocks
A block is nothing more than a struct namespace that specifies two
things:
- The type(s) that should be (de)serialized by the block.
- How to serialize or deserialize into values of those types.
There are a few different kinds of blocks you can make in Getty, so let's go over them now.
Serialization Blocks
To manually define the serialization process for a type, you can use a serialization block.
const sb = struct {
// (1)!
pub fn is(comptime T: type) bool {
return T == bool;
}
// (2)!
pub fn serialize(
allocator: ?std.mem.Allocator,
value: anytype,
serializer: anytype,
) @TypeOf(serializer).Error!@TypeOf(serializer).Ok {
_ = allocator;
// Convert bool value to a Getty Integer.
const v: i32 = if (value) 1 else 0;
// Pass the Getty Integer value to the serializer.
return try serializer.serializeInt(v);
}
};
-
isspecifies which types can be serialized by thesbblock.
In this case, thesbblock applies only toboolvalues. -
serializespecifies how to serialize values relevant to thesbblock into Getty's data model.
In this case, we're telling Getty to serializeboolvalues as Integers.
Deserialization Blocks
To manually define the deserialization process for a type, you can use a deserialization block.
const db = struct {
// (1)!
pub fn is(comptime T: type) bool {
return T == bool;
}
// (2)!
pub fn deserialize(
allocator: ?std.mem.Allocator,
comptime T: type,
deserializer: anytype,
visitor: anytype,
) @TypeOf(deserializer).Error!@TypeOf(visitor).Value {
_ = T; // (3)!
return try deserializer.deserializeInt(allocator, visitor);
}
// (4)!
pub fn Visitor(comptime Value: type) type {
return struct {
pub usingnamespace getty.de.Visitor(
@This(),
Value,
.{ .visitInt = visitInt },
);
pub fn visitInt(
self: @This(),
allocator: ?std.mem.Allocator,
comptime Deserializer: type,
input: anytype,
) Deserializer.Error!Value {
_ = self;
_ = allocator;
return input != 0;
}
};
}
};
-
isspecifies which types can be deserialized into by thedbblock.
In this case, thedbblock applies only toboolvalues. -
deserializespecifies the hint that Getty should provide a deserializer about the type being deserialized into.
In this case, we calldeserializeInt, which means that Getty will tell the deserializer that the Zig type being deserialized into can probably be made from a Getty Integer. -
Tis the current type being deserialized into.
Usually, you don't need it unless you're doing pointer deserialization. -
Visitoris a generic type that implementsgetty.de.Visitor.
Visitors are responsible for specifying how to deserialize values from Getty's data model into Zig. In this case, our visitor can deserialize Integers intoboolvalues, which it does by simply returning whether or not the integer is 0.
Attribute Blocks
SBs and DBs are typically used for complex modifications to Getty's (de)serialization processes. For simpler customizations, you can usually get away with the more convenient attribute blocks.
Compatibility
Attribute blocks may only be defined by struct and union types.
With ABs, Getty's default (de)serialization processes are used. For
example, struct values would be serialized using the default
getty.ser.blocks.Struct block and deserialized with the default
getty.de.blocks.Struct block. However, based on the attributes that you
specify, slight changes to these default processes will take effect.
Regardless of whether you're serializing or deserializing, ABs are always defined like so:
const Point = struct {
x: i32,
y: i32 = 123,
};
const ab = struct {
pub fn is(comptime T: type) bool {
return T == Point;
}
// (1)!
pub const attributes = .{ // (2)!
.x = .{ .rename = "X" }, // (3)!
.y = .{ .skip = true },
};
};
-
attributesspecifies various (de)serialization properties for values relevant to theabblock.
Ifabis used for serialization, thenattributesspecifies that thexfield ofPointshould be serialized as"X", and that theyfield ofPointshould be skipped.
Ifabis used for deserialization, thenattributesspecifies that the value for thexfield ofPointhas been serialized as"X", and that theyfield ofPointshould not be deserialized.
-
attributesis an anonymous struct literal.
Each field name inattributesmust match either a field or variant in yourstructorunion, or the wordContainer. The former are known as field/variant attributes, while the latter are known as container attributes. -
Each field in
attributesis also an anonymous struct literal. The fields in these innerstructvalues depend on the kind of attribute you're specifying (i.e., field/variant or container).
Supported Attributes
For a complete list of the attributes supported by Getty, see here.
Type-Defined Blocks
The blocks we've discussed so far are known as out-of-band blocks. They're
defined separately from the type(s) that they operate on. Out-of-band blocks have
their place, such as when you want to customize a type that you didn't define
(e.g., the types in std). However, there's a more convenient way to do
things for struct and union types that you did define yourself.
If you define a block within a struct or union, Getty will automatically
process it without you having to pass it to a (de)serializer. All you have to
do is make sure that the block is public and named @"getty.sb" (for serialization)
or @"getty.db" (for deserialization).
Type-defined blocks are defined exactly the same as attribute and
(de)serialization blocks are. The only difference is that you don't need to
define an is function.
const Point = struct {
x: i32,
y: i32,
pub const @"getty.sb" = struct {
pub const attributes = .{
.x = .{ .rename = "X" },
.y = .{ .skip = true },
};
};
};
Usage
Once you've defined a block, you can pass them along to Getty via the
getty.Serializer and
getty.Deserializer interfaces.
They take optional (de)serialization blocks as arguments.
For example, the following defines a serializer that can serialize Booleans and Integers into JSON. It's generic over an SB, which it passes to Getty, making it even easier for us to customize Getty's behavior.
const std = @import("std");
const getty = @import("getty");
fn Serializer(comptime user_sb: anytype) type {
return struct {
pub usingnamespace getty.Serializer(
@This(),
Ok,
Error,
user_sb,
null,
null,
null,
null,
.{
.serializeBool = serializeBool,
.serializeInt = serializeInt,
},
);
const Ok = void;
const Error = getty.ser.Error;
fn serializeBool(_: @This(), value: bool) Error!Ok {
std.debug.print("{}\n", .{value});
}
fn serializeInt(_: @This(), value: anytype) Error!Ok {
std.debug.print("{}\n", .{value});
}
};
}
const sb = struct {
pub fn is(comptime T: type) bool {
return T == bool;
}
pub fn serialize(_: ?std.mem.Allocator, value: anytype, serializer: anytype) !@TypeOf(serializer).Ok {
const v: i32 = if (value) 1 else 0;
return try serializer.serializeInt(v);
}
};
pub fn main() !void {
// Normal
{
var s = Serializer(null){};
const serializer = s.serializer();
try getty.serialize(null, true, serializer);
try getty.serialize(null, false, serializer);
}
// Custom
{
var s = Serializer(sb){};
const serializer = s.serializer();
try getty.serialize(null, true, serializer);
try getty.serialize(null, false, serializer);
}
}
Tuples
In order to pass multiple (de)serialization blocks to Getty, you can use (de)serialization tuples.
A (de)serialization tuple is, well, a tuple of (de)serialization blocks. They can be used wherever a (de)serialization block can be used and allow you to do some pretty cool things. For example, suppose you had the following type:
If all you wanted to do was serialize Point values as Sequences, you'd
just write an SB and pass it along to Getty. However, what if you also wanted
to serialize i32 values as Booleans? One option is to stuff all of
your custom serialization logic into a single block. But that gets messy really
quick and inevitably becomes a pain to maintain.
A much better solution is to break up your serialization logic into separate
blocks. One for Point values and one for i32 values. Then, you just
group them together as a serialization tuple!