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);
}
};
-
is
specifies which types can be serialized by thesb
block.
In this case, thesb
block applies only tobool
values. -
serialize
specifies how to serialize values relevant to thesb
block into Getty's data model.
In this case, we're telling Getty to serializebool
values 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;
}
};
}
};
-
is
specifies which types can be deserialized into by thedb
block.
In this case, thedb
block applies only tobool
values. -
deserialize
specifies 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. -
T
is the current type being deserialized into.
Usually, you don't need it unless you're doing pointer deserialization. -
Visitor
is 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 intobool
values, 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 },
};
};
-
attributes
specifies various (de)serialization properties for values relevant to theab
block.
Ifab
is used for serialization, thenattributes
specifies that thex
field ofPoint
should be serialized as"X"
, and that they
field ofPoint
should be skipped.
Ifab
is used for deserialization, thenattributes
specifies that the value for thex
field ofPoint
has been serialized as"X"
, and that they
field ofPoint
should not be deserialized.
-
attributes
is an anonymous struct literal.
Each field name inattributes
must match either a field or variant in yourstruct
orunion
, or the wordContainer
. The former are known as field/variant attributes, while the latter are known as container attributes. -
Each field in
attributes
is also an anonymous struct literal. The fields in these innerstruct
values 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!