Serialize supported Zig values into one contiguous byte buffer, then read them through checked or unchecked typed views.
This is useful when you want:
- Compact binary snapshots of nested data.
- Fast field/slice access without fully decoding everything.
- A clear trust boundary (
Untrusted->validate()->Trusted).
Warning
Experimental: API and wire details may change.
Trust model: Always treat incoming bytes as untrusted until validated.
Portability: Wire format uses native type representation (for example endianness and tag layout), so cross-platform compatibility is limited unless environments match.
Many serializers force full decoding before you can inspect anything. oneserial gives you typed views so you can:
- Validate once.
- Traverse fields/slices directly from the byte buffer.
- Decode to owned memory only when needed.
const std = @import("std");
const oneserial = @import("oneserial");
const Msg = struct {
id: u64,
user: struct {
name: []const u8,
level: u8,
},
tags: []const []const u8,
maybe_ptr: ?*const u32,
payload: union(enum) {
text: []const u8,
code: u16,
none: void,
},
result: error{Offline}![]const u8,
};
pub fn main() !void {
const gpa = std.heap.page_allocator;
var n: u32 = 7;
const msg = Msg{
.id = 42,
.user = .{ .name = "zig", .level = 3 },
.tags = &.{ "alpha", "beta", "gamma" },
.maybe_ptr = &n,
.payload = .{ .text = "hello" },
.result = "ok",
};
var wrapper = try oneserial.Wrapper(Msg, .{}).init(&msg, gpa);
defer wrapper.deinit(gpa);
// Validate once, then use trusted typed views.
const trusted = try wrapper.untrusted().validate();
const id = trusted.field("id").value();
const tags = trusted.field("tags");
std.debug.print("id={d}, tags={d}\n", .{ id, tags.len() });
}- Add dependency in
build.zig.zon:
.dependencies = .{
.oneserial = .{
.url = "git+https://github.com/SmallThingz/oneserial#<commit>",
.hash = "<hash>",
},
},- Add module import in
build.zig:
const dep = b.dependency("oneserial", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("oneserial", dep.module("oneserial"));oneserial.Converter(T, .{})- Entry point for per-type operations.
oneserial.serializeAlloc(T, .{}, &value, allocator)- Serialize value into one aligned byte buffer.
oneserial.Wrapper(T, .{})- Owns serialized bytes and provides
.untrusted().
- Owns serialized bytes and provides
oneserial.Untrusted(T, .{})- Checked access. Call
.validate()for full-buffer validation.
- Checked access. Call
oneserial.Trusted(T, .{})- Assumes bytes are valid; cheaper typed access.
MergeOptions has endian (default: native). Use it when producing or consuming non-native wire bytes:
const opposite: std.builtin.Endian = if (@import("builtin").target.cpu.arch.endian() == .little) .big else .little;
const bytes = try oneserial.serializeAlloc(MyType, .{ .endian = opposite }, &value, allocator);
const u = oneserial.Untrusted(MyType, .{}).init(bytes).withEndian(opposite);
const trusted = try u.validate();Available on Untrusted, Trusted, and nested views as type-appropriate:
.field("name")for structs.get()for dynamic values.len()/.at(i)/.atUnchecked(i)for slices.value()to decode the current view value.toOwned(allocator)to allocate and decode owned value
- Primitives:
void,bool, integers, floats, vectors,null - Containers: arrays, structs, tagged unions, optionals, error unions
- Indirection:
*T(one pointers) and[]T(slices) - Enums
[*]Tand[*c]T- Untagged unions
type,noreturn, comptime-only value types,opaque, standaloneerror_set, function/frame types
- Recursive types are supported, but recursive cyclic data is not. Cycles can recurse forever.
- This format is intentionally low-level and destructive: it prioritizes speed and simple traversal over schema evolution guarantees.
Trustedaccess should only be used after validation or in already-trusted contexts.
Run tests:
zig build test