Hello,
I’ve been working on a serialization library that gives a single
API across JSON, MessagePack, TOML, YAML, ZON, and CSV. All six
formats support both serialize and deserialize.
The core idea: define a struct, call serde.json.toSlice or
serde.yaml.fromSlice, same pattern everywhere. Swap the format,
keep your code.
const bytes = try serde.toml.toSlice(allocator, config);
const cfg = try serde.toml.fromSlice(Config, arena.allocator(), input);
Uses @typeInfo to walk struct fields at comptime. Supports nested
structs, optionals, tagged unions, enums, slices, StringHashMap,
pointers, tuples, void.
Serde options
You can declare pub const serde_options on your types to
customize behavior. Everything resolved at comptime.
Field renaming and naming conventions:
const User = struct {
user_id: u64,
first_name: []const u8,
pub const serde_options = .{
.rename = .{ .user_id = "id" },
.rename_all = serde.NamingConvention.camel_case,
};
};
// => {"id":1,"firstName":"Alice"}
Available conventions: .camel_case, .snake_case, .pascal_case,.kebab_case, .SCREAMING_SNAKE_CASE
Skip fields:
pub const serde_options = .{
.skip = .{
.token = serde.SkipMode.always, // never serialize
.email = serde.SkipMode.@"null", // skip if null
.tags = serde.SkipMode.empty, // skip if empty slice
},
};
Flatten nested structs:
const User = struct {
name: []const u8,
meta: Metadata,
pub const serde_options = .{
.flatten = &[_][]const u8{"meta"},
};
};
// {"name":"Alice","created_by":"admin","version":2}
// instead of {"name":"Alice","meta":{"created_by":"admin",...}}
Union tagging styles (external, internal, adjacent, untagged):
const Command = union(enum) {
ping: void,
execute: struct { query: []const u8 },
pub const serde_options = .{
.tag = serde.UnionTag.internal,
.tag_field = "type",
};
};
// .external: {"execute":{"query":"SELECT 1"}}
// .internal: {"type":"execute","query":"SELECT 1"}
// .adjacent: {"type":"execute","content":{"query":"SELECT 1"}}
// .untagged: {"query":"SELECT 1"}
Enum as integer:
const Status = enum(u8) {
active = 0, inactive = 1,
pub const serde_options = .{
.enum_repr = serde.EnumRepr.integer,
};
};
// serializes as 0, 1 instead of "active", "inactive"
Per-field custom serialization:
pub const serde_options = .{
.with = .{
.created_at = serde.helpers.UnixTimestampMs,
},
};
Built-in helpers: UnixTimestamp, UnixTimestampMs, Base64
Deny unknown fields:
pub const serde_options = .{
.deny_unknown_fields = true,
};
For full control you can also declare zerdeSerialize /
zerdeDeserialize methods directly on your type.
There’s also zero-copy JSON deserialization via fromSliceBorrowed
- when strings have no escape sequences, returned slices point
directly into the input buffer.
Requires Zig 0.15.0.
Would appreciate feedback on the API, missing type support, or
anything that feels off