Serde.zig
Universal serialization for Zig: JSON, Yaml, XML, MessagePack, TOML, CSV and more from a single API. msgpack.org[Zig]
Install / Use
/learn @OrlovEvgeny/Serde.zigREADME
serde.zig
Serialization framework for Zig
Uses Zig's comptime reflection (@typeInfo) to serialize and deserialize any Zig type across JSON, MessagePack, TOML, YAML, XML, ZON, and CSV without macros, code generation, or runtime type information.
Table of Contents
- Why serde.zig?
- Quick Start
- Installation
- Formats
- Supported Types
- Examples
- Serde Options
- Out-of-Band Schema
- Out-of-Band Type Overrides
- Custom Serialization
- Error Handling
- Tests
- License
Why serde.zig?
No boilerplate. No macros, no code generation, no build steps. Just declare a struct and serialize it. Zig's comptime reflection handles everything at compile time.
Seven formats, one API. JSON, MessagePack, TOML, YAML, XML, ZON, and CSV all share the same toSlice/fromSlice/toWriter/fromReader interface. Learn once, use everywhere.
Out-of-band schemas. Serialize the same type differently in different contexts without modifying the type itself. Essential for third-party types and API versioning.
Zero-copy JSON. fromSliceBorrowed returns string slices that point directly into the input buffer when no escape sequences are present. No allocation, no copying.
Comptime validation. Invalid types, missing fields, and incorrect option names are caught at compile time, not at runtime.
Quick Start
const serde = @import("serde");
const User = struct {
name: []const u8,
age: u32,
email: ?[]const u8 = null,
};
// Serialize to JSON
const json_bytes = try serde.json.toSlice(allocator, User{
.name = "Alice",
.age = 30,
.email = "alice@example.com",
});
// => {"name":"Alice","age":30,"email":"alice@example.com"}
// Deserialize from JSON
const user = try serde.json.fromSlice(User, allocator, json_bytes);
Installation
Latest version from master:
zig fetch --save git+https://github.com/OrlovEvgeny/serde.zig
Specific release:
zig fetch --save https://github.com/OrlovEvgeny/serde.zig/archive/refs/tags/v1.0.2.tar.gz
Then in your build.zig:
const serde_dep = b.dependency("serde", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("serde", serde_dep.module("serde"));
Requires Zig 0.15.0 or later.
Formats
| Format | Module | Serialize | Deserialize |
|--------|--------|-----------|-------------|
| JSON | serde.json | + | + |
| MessagePack | serde.msgpack | + | + |
| TOML | serde.toml | + | + |
| YAML | serde.yaml | + | + |
| XML | serde.xml | + | + |
| ZON | serde.zon | + | + |
| CSV | serde.csv | + | + |
Every format exposes the same API:
// Serialization
const bytes = try serde.json.toSlice(allocator, value);
try serde.json.toWriter(&writer, value);
// Deserialization
const val = try serde.json.fromSlice(T, allocator, bytes);
const val = try serde.json.fromReader(T, allocator, &reader);
Supported Types
bool,i8..i128,u8..u128,f16..f128[]const u8,[]u8,[:0]const u8(strings)?T(optionals, serialized as value or null)[N]T(fixed-length arrays)[]T,[]const T(slices)- Structs with named fields, nested arbitrarily
- Tuples (
struct { i32, bool }, serialized as arrays) - Enums (as string name or integer)
- Tagged unions (
union(enum), four tagging styles) *T,*const T(pointers, followed transparently)std.StringHashMap(V)(maps)void(serialized as null)
Examples
Nested structs
const Address = struct {
street: []const u8,
city: []const u8,
zip: []const u8,
};
const Person = struct {
name: []const u8,
age: u32,
address: Address,
tags: []const []const u8,
};
const person = Person{
.name = "Bob",
.age = 25,
.address = .{ .street = "123 Main St", .city = "Springfield", .zip = "62704" },
.tags = &.{ "admin", "active" },
};
const json = try serde.json.toSlice(allocator, person);
const msgpack = try serde.msgpack.toSlice(allocator, person);
const yaml = try serde.yaml.toSlice(allocator, person);
const xml = try serde.xml.toSlice(allocator, person);
Arena allocator (recommended for deserialization)
Deserialization allocates memory for strings, slices, and nested structures. Use an ArenaAllocator for easy cleanup:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const user = try serde.json.fromSlice(User, arena.allocator(), json_bytes);
Zero-copy deserialization
When strings in the JSON input contain no escape sequences, fromSliceBorrowed returns slices pointing directly into the input buffer:
const input = "{\"name\":\"alice\",\"id\":1}";
const msg = try serde.json.fromSliceBorrowed(Msg, allocator, input);
// msg.name points into input, input must outlive msg
Pretty-printed output
const pretty = try serde.json.toSliceWith(allocator, value, .{ .pretty = true, .indent = 2 });
// {
// "name": "Alice",
// "age": 30
// }
Tagged unions
const Command = union(enum) {
ping: void,
execute: struct { query: []const u8 },
quit: void,
};
const cmd = Command{ .execute = .{ .query = "SELECT 1" } };
const bytes = try serde.json.toSlice(allocator, cmd);
// => {"execute":{"query":"SELECT 1"}}
Enums
const Color = enum { red, green, blue };
const bytes = try serde.json.toSlice(allocator, Color.blue);
// => "blue"
const color = try serde.json.fromSlice(Color, allocator, bytes);
// => Color.blue
Maps
var map = std.StringHashMap(i32).init(allocator);
defer map.deinit();
try map.put("a", 1);
try map.put("b", 2);
const bytes = try serde.json.toSlice(allocator, map);
// => {"a":1,"b":2}
CSV
const Record = struct {
name: []const u8,
age: u32,
active: bool,
};
const records: []const Record = &.{
.{ .name = "Alice", .age = 30, .active = true },
.{ .name = "Bob", .age = 25, .active = false },
};
const csv_bytes = try serde.csv.toSlice(allocator, records);
// name,age,active
// Alice,30,true
// Bob,25,false
TOML
const Config = struct {
title: []const u8,
port: u16 = 8080,
database: struct {
host: []const u8,
name: []const u8,
},
};
const cfg = try serde.toml.fromSlice(Config, arena.allocator(),
\\title = "myapp"
\\port = 3000
\\
\\[database]
\\host = "localhost"
\\name = "mydb"
);
YAML
const Server = struct {
host: []const u8,
port: u16,
debug: bool,
};
const yaml_input =
\\host: localhost
\\port: 8080
\\debug: true
;
const server = try serde.yaml.fromSlice(Server, arena.allocator(), yaml_input);
const yaml_bytes = try serde.yaml.toSlice(allocator, server);
// host: localhost
// port: 8080
// debug: true
XML
const User = struct {
id: u64,
name: []const u8,
role: []const u8,
pub const serde = .{
.xml_attribute = .{.id},
.xml_root = "user",
};
};
const xml_bytes = try serde.xml.toSlice(allocator, User{
.id = 42,
.name = "Alice",
.role = "admin",
});
// <?xml version="1.0" encoding="UTF-8"?>
// <user id="42"><name>Alice</name><role>admin</role></user>
const user = try serde.xml.fromSlice(User, arena.allocator(), xml_bytes);
Fields listed in xml_attribute are serialized as XML attributes on the root element. All other fields become child elements.
ZON
Produces valid .zon files:
const bytes = try serde.zon.toSlice(allocator, Config{
.title = "myapp",
.port = 3000,
.database = .{ .host = "localhost", .name = "mydb" },
});
// .{
// .title = "myapp",
// .port = 3000,
// .database = .{
// .host = "localhost",
// .name = "mydb",
// },
// }
Serde Options
Customize serialization behavior by declaring pub const serde on your types. All options are resolved at comptime.
Field renaming
const User = struct {
user_id: u64,
first_name: []const u8,
last_name: []const u8,
pub const serde = .{
.rename = .{ .user_id = "id" },
.rename_all = serde.NamingConvention.camel_case,
};
};
// Serializes as: {"id":1,"firstName":"Alice","lastName":"Smith"}
Available conventions: .camel_case, .snake_case, .pascal_case, .kebab_case, .SCREAMING_SNAKE_CASE.
Asymmetric renaming
Use different names for serialization and deserialization. This is essential for API evolution,
Related Skills
node-connect
353.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
353.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
353.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
