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.
conststd=@import("std");
constoneserial=@import("oneserial");
constMsg=struct {
id: u64,
user: struct {
name: []constu8,
level: u8,
},
tags: []const []constu8,
maybe_ptr: ?*constu32,
payload: union(enum) {
text: []constu8,
code: u16,
none: void,
},
result: union(enum) { ok: []constu8, offline: void },
};
pubfnmain() !void {
constgpa=std.heap.page_allocator;
varn: u32=7;
constmsg=Msg{
.id=42,
.user= .{ .name="zig", .level=3 },
.tags= &.{ "alpha", "beta", "gamma" },
.maybe_ptr=&n,
.payload= .{ .text="hello" },
.result= .{ .ok="ok" },
};
varwrapper=tryoneserial.Wrapper(Msg, .{}).init(&msg, gpa);
deferwrapper.deinit(gpa);
// Validate once, then use trusted typed views.consttrusted=trywrapper.untrusted().validate();
constid=trusted.field("id").value();
consttags=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:
constdep=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.allocFromShim(T, .{}, &shim, allocator)- Allocate dynamic shape from a shim value without copying payload bytes.
oneserial.invalidPointer(P)- Pointer sentinel for shim fields where recursion should stop.
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:
constopposite: std.builtin.Endian=if (@import("builtin").target.cpu.arch.endian() ==.little) .bigelse.little;
constbytes=tryoneserial.serializeAlloc(MyType, .{ .endian=opposite }, &value, allocator);
constu=oneserial.Untrusted(MyType, .{}).init(bytes).withEndian(opposite);
consttrusted=tryu.validate();allocFromShim is useful when you only know allocation shape (lengths/presence/tag) and want to fill payload bytes later.
constT=struct {
a: []constu8,
b: []constu8,
};
consts=oneserial.invalidPointer([*]constu8);
constshim=T{
.a=s[0..8],
.b=s[0..32],
};
constout=tryoneserial.allocFromShim(T, .{}, &shim, allocator);
// out.a/out.b are allocated with matching lengths; payload bytes are not copied.Nested shapes are supported:
constT=struct { a: []const []constu8 };
consts=oneserial.invalidPointer([*]constu8);
constinner= [_][]constu8{
s[0..4],
s[0..2],
};
constshim=T{ .a=inner[0..] };
constout=tryoneserial.allocFromShim(T, .{}, &shim, allocator);When a pointer (or slice .ptr) equals invalidPointer(...), OneSerial allocates that container but does not recurse deeper into pointee/element payloads.
For []const []const u8 specifically:
- Outer slice sentinel (
[*]const []const u8) withlen = NallocatesNinner-slice headers. - Inner
[]const u8element sentinels allocate each inner byte buffer by itslen.
Important
Values returned by allocFromShim may contain undefined non-shape data.
You must initialize payload data before reading it.
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
- Indirection:
*T(one pointers) and[]T(slices) - Enums
[*]Tand[*c]Terroranderror_unionvalues- 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