# std.zon - ZON Parsing and Serialization ZON ("Zig Object Notation") parsing and stringification. Its grammar is a subset of Zig's syntax except for the supported `nan` and `inf` literals. In Zig 0.16, file examples use `std.Io.Dir`/`std.Io.File` and an explicit `std.Io`. Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html ## Table of Contents - [ZON Format Overview](#zon-format-overview) - [Parsing ZON](#parsing-zon) - [Serializing to ZON](#serializing-to-zon) - [Low-Level Serializer API](#low-level-serializer-api) - [Supported Types](#supported-types) - [Common Patterns](#common-patterns) ## ZON Format Overview ZON is a data format using Zig's literal syntax: ```zig // Example ZON file .{ .name = "my-project", .version = .{ 0, 1, 0 }, .dependencies = .{ .@"std-lib" = .{ .url = "https://...", .hash = "abc123" }, }, .build_options = .{ .optimize = .release_safe, .strip = true, }, } ``` ### Supported Primitives - Boolean literals: `true`, `false` - Number literals: `42`, `-3.14`, `0xFF`, `nan`, `inf`, `-inf` - Character literals: `'a'`, `'\n'`, `'\u{1F600}'` - Enum literals: `.foo`, `.bar` - `null` literal - String literals: `"hello"`, multiline strings ### Supported Containers - Anonymous struct literals: `.{ .x = 1, .y = 2 }` - Anonymous tuple literals: `.{ 1, 2, 3 }` **Note:** ZON may not contain type names. Use `@import` for compile-time ZON parsing. ## Parsing ZON ### Parse into Struct (Runtime) ```zig const std = @import("std"); const Config = struct { name: []const u8, port: u16 = 8080, debug: bool = false, }; const zon_str: [:0]const u8 = \\.{ \\ .name = "server", \\ .port = 3000, \\} ; pub fn main() !void { var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const config = try std.zon.parse.fromSliceAlloc(Config, allocator, zon_str, null, .{}); defer std.zon.parse.free(allocator, config); // config.name == "server" // config.port == 3000 // config.debug == false (default) } ``` ### Parse with Diagnostics ```zig var diag: std.zon.parse.Diagnostics = .{}; defer diag.deinit(allocator); const result = std.zon.parse.fromSliceAlloc(Config, allocator, zon_str, &diag, .{}) catch |err| { // Print diagnostic errors var errors = diag.iterateErrors(); while (errors.next()) |parse_err| { const loc = parse_err.getLocation(&diag); std.debug.print("{d}:{d}: {f}\n", .{ loc.line + 1, loc.column + 1, parse_err.fmtMessage(&diag), }); } return err; }; defer std.zon.parse.free(allocator, result); ``` ### Parse Options ```zig const result = try std.zon.parse.fromSliceAlloc(T, allocator, zon_str, diag, .{ // Ignore unknown fields (default: false - errors on unknown) .ignore_unknown_fields = true, // Free partially parsed values on error (default: true) // Disable if using arena allocation .free_on_error = false, }); ``` ### Compile-Time Parsing with @import ```zig // build.zig.zon is automatically imported at comptime const build_zon = @import("build.zig.zon"); // Access fields directly const name = build_zon.name; const version = build_zon.version; ``` ### Free Parsed Values ```zig const result = try std.zon.parse.fromSliceAlloc(T, allocator, zon_str, null, .{}); defer std.zon.parse.free(allocator, result); ``` Use `fromSlice` only when `T` contains no pointers; that result owns no allocations and needs no `free`. Use `fromSliceAlloc` for pointer-containing values such as structs with string slices, and release the result with `std.zon.parse.free`. ## Serializing to ZON ### Simple Serialization ```zig const std = @import("std"); const Config = struct { name: []const u8, port: u16, enabled: bool, }; pub fn main() !void { var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const config = Config{ .name = "server", .port = 8080, .enabled = true, }; // Serialize to allocated buffer var aw: std.Io.Writer.Allocating = .init(allocator); defer aw.deinit(); try std.zon.stringify.serialize(config, .{}, &aw.writer); const zon_str = aw.written(); // .{ // .name = "server", // .port = 8080, // .enabled = true, // } } ``` ### Serialize Options ```zig try std.zon.stringify.serialize(value, .{ // Include whitespace for readability (default: true) .whitespace = true, // false for minified output // Emit codepoints as character literals (default: .never) .emit_codepoint_literals = .never, // always emit as integers // .emit_codepoint_literals = .printable_ascii, // 'a' for printable ASCII // .emit_codepoint_literals = .always, // '⚡' for all valid codepoints // Emit []u8 as tuple instead of string (default: false) .emit_strings_as_containers = false, // Skip fields equal to their default value (default: true) .emit_default_optional_fields = true, // false to omit defaults }, &writer); ``` ### Serialization with Depth Limits (Recursive Types) ```zig // For potentially recursive types, use depth-limited versions: // Returns error.ExceededMaxDepth if depth exceeded try std.zon.stringify.serializeMaxDepth(value, .{}, &writer, 16); // No depth checking - caller must ensure no cycles try std.zon.stringify.serializeArbitraryDepth(value, .{}, &writer); ``` ## Low-Level Serializer API Use `std.zon.Serializer` for fine-grained control over output. ### Manual Struct Serialization ```zig var aw: std.Io.Writer.Allocating = .init(allocator); defer aw.deinit(); var s: std.zon.Serializer = .{ .writer = &aw.writer }; var container = try s.beginStruct(.{}); try container.field("x", 10, .{}); try container.field("y", 20, .{}); try container.field("name", "point", .{}); try container.end(); // Output: .{ // .x = 10, // .y = 20, // .name = "point", // } ``` ### Manual Tuple Serialization ```zig var s: std.zon.Serializer = .{ .writer = &aw.writer }; var tuple = try s.beginTuple(.{}); try tuple.field(1, .{}); try tuple.field(2, .{}); try tuple.field(3, .{}); try tuple.end(); // Output: .{ // 1, // 2, // 3, // } ``` ### Container Options ```zig // Control wrapping behavior var container = try s.beginStruct(.{ .whitespace_style = .{ .wrap = true }, // Always wrap fields // .whitespace_style = .{ .wrap = false }, // Never wrap (single line) // .whitespace_style = .{ .fields = 2 }, // caller says this container has 2 fields; no wrap }); ``` For `.fields = n`, the caller supplies the expected field count. Values greater than two select wrapped output; the serializer does not look ahead and count future fields. ### Nested Containers ```zig var s: std.zon.Serializer = .{ .writer = &aw.writer }; var root = try s.beginStruct(.{}); // Nested tuple var coords = try root.beginTupleField("coords", .{}); try coords.field(10, .{}); try coords.field(20, .{}); try coords.end(); // Nested struct var meta = try root.beginStructField("meta", .{}); try meta.field("id", 42, .{}); try meta.end(); try root.end(); // Output: .{ // .coords = .{ // 10, // 20, // }, // .meta = .{ // .id = 42, // }, // } ``` ### Primitive Serialization ```zig var s: std.zon.Serializer = .{ .writer = &aw.writer }; // Integer try s.int(42); // Float try s.float(3.14); // String try s.string("hello\nworld"); // "hello\nworld" // Multiline string try s.multilineString("line1\nline2", .{}); // \\line1 // \\line2 // Identifier/enum literal try s.ident("foo"); // .foo try s.ident("var"); // .@"var" (escaped keyword) // Unicode codepoint try s.codePoint('a'); // 'a' try s.codePoint('⚡'); // '\u{26a1}' ``` ### Value Serialization with Options ```zig var s: std.zon.Serializer = .{ .writer = &aw.writer }; try s.value(my_value, .{ .emit_codepoint_literals = .always, .emit_strings_as_containers = false, .emit_default_optional_fields = true, }); ``` ## Supported Types ### Parse-able Types | Zig Type | ZON Syntax | |----------|------------| | `bool` | `true`, `false` | | `i32`, `u64`, etc. | `42`, `-5`, `0xFF` | | `f32`, `f64` | `3.14`, `-0.0`, `nan`, `inf` | | `?T` | value or `null` | | `[]const u8` | `"string"`, multiline strings | | `[]T` | `.{ item1, item2, ... }` | | `[N]T` | `.{ item1, item2, ... }` (exact length) | | `struct` | `.{ .field = value, ... }` | | `struct (tuple)` | `.{ value1, value2, ... }` | | `union(enum)` | `.tag` or `.{ .tag = value }` | | `enum` | `.variant` | | `*T` | value (auto-allocated) | | `@Vector(N, T)` | `.{ elem1, elem2, ... }` | ### Non-serializable Types These types cannot be serialized: - `type`, `void` (except as union payload), `noreturn` - Error sets/error unions - Untagged unions - Non-exhaustive enums - Many-pointers (`[*]T`) or C-pointers (`[*c]T`) - Opaque types (`anyopaque`) - Async frame types (`anyframe`) - Functions ## Common Patterns ### Build Configuration File ```zig // build.zig.zon .{ .name = "my-project", .version = "0.1.0", .dependencies = .{ .zap = .{ .url = "https://github.com/...", .hash = "...", }, }, .paths = .{ "src", "build.zig", "build.zig.zon" }, } ``` ```zig // build.zig - reading build.zig.zon at comptime const build_zon = @import("build.zig.zon"); const project_name = build_zon.name; ``` ### Config File with Defaults ```zig const Config = struct { host: []const u8 = "localhost", port: u16 = 8080, workers: u8 = 4, debug: bool = false, }; fn loadConfig(io: std.Io, allocator: std.mem.Allocator, path: []const u8) !Config { const content = std.Io.Dir.cwd().readFileAllocOptions( io, path, allocator, .limited(1024 * 1024), .of(u8), 0, ) catch |err| switch (err) { error.FileNotFound => return Config{}, else => return err, }; defer allocator.free(content); return std.zon.parse.fromSliceAlloc(Config, allocator, content, null, .{ .ignore_unknown_fields = true, .free_on_error = true, }); } ``` The caller owns a successfully loaded `Config` and should pass it to `std.zon.parse.free` when finished. ### Serialize to File ```zig fn saveConfig(io: std.Io, config: Config, path: []const u8) !void { const file = try std.Io.Dir.cwd().createFile(io, path, .{}); defer file.close(io); var buffer: [4096]u8 = undefined; var file_writer = file.writer(io, &buffer); try std.zon.stringify.serialize(config, .{ .whitespace = true }, &file_writer.interface); try file_writer.interface.flush(); } ``` ### Union Serialization ```zig const Value = union(enum) { int: i64, float: f64, string: []const u8, none, // void payload }; const v1 = Value{ .int = 42 }; // Serializes as: .{ .int = 42 } const v2 = Value.none; // Serializes as: .none ``` ### Skip Default Fields ```zig const Settings = struct { theme: []const u8 = "dark", font_size: u8 = 12, custom_value: u32, }; const settings = Settings{ .custom_value = 100 }; try std.zon.stringify.serialize(settings, .{ .emit_default_optional_fields = false, }, &writer); // Output: .{ .custom_value = 100 } // (theme and font_size omitted because they equal defaults) ``` ### Round-Trip ZON Data ```zig fn roundTripAlloc(comptime T: type, allocator: std.mem.Allocator, value: T) !T { // Serialize var aw: std.Io.Writer.Allocating = .init(allocator); defer aw.deinit(); try std.zon.stringify.serialize(value, .{}, &aw.writer); // Add null terminator for parsing try aw.writer.writeByte(0); const zon_str = aw.written(); const terminated: [:0]const u8 = zon_str[0 .. zon_str.len - 1 :0]; // Parse back return std.zon.parse.fromSliceAlloc(T, allocator, terminated, null, .{}); } ``` The returned value may own allocations; the caller must eventually call `std.zon.parse.free(allocator, result)`. For a pointer-free `T`, a `fromSlice`-based variant can return a value that needs no cleanup.