Commit c3da98cf5a

Andrew Kelley <andrew@ziglang.org>
2025-07-20 01:59:16
std.zon: update to new I/O API
1 parent b956b02
Changed files (5)
lib/std/Build/Cache/Path.zig
@@ -161,17 +161,19 @@ pub fn formatEscapeString(path: Path, writer: *std.io.Writer) std.io.Writer.Erro
     }
 }
 
+/// Deprecated, use double quoted escape to print paths.
 pub fn fmtEscapeChar(path: Path) std.fmt.Formatter(Path, formatEscapeChar) {
     return .{ .data = path };
 }
 
+/// Deprecated, use double quoted escape to print paths.
 pub fn formatEscapeChar(path: Path, writer: *std.io.Writer) std.io.Writer.Error!void {
     if (path.root_dir.path) |p| {
-        try std.zig.charEscape(p, writer);
-        if (path.sub_path.len > 0) try std.zig.charEscape(fs.path.sep_str, writer);
+        for (p) |byte| try std.zig.charEscape(byte, writer);
+        if (path.sub_path.len > 0) try writer.writeByte(fs.path.sep);
     }
     if (path.sub_path.len > 0) {
-        try std.zig.charEscape(path.sub_path, writer);
+        for (path.sub_path) |byte| try std.zig.charEscape(byte, writer);
     }
 }
 
lib/std/zig/Ast.zig
@@ -574,7 +574,7 @@ pub fn renderError(tree: Ast, parse_error: Error, w: *Writer) Writer.Error!void
                     '/' => "comment",
                     else => unreachable,
                 },
-                std.zig.fmtChar(tok_slice[parse_error.extra.offset..][0..1]),
+                std.zig.fmtChar(tok_slice[parse_error.extra.offset]),
             });
         },
 
lib/std/zon/parse.zig
@@ -64,14 +64,14 @@ pub const Error = union(enum) {
             }
         };
 
-        fn formatMessage(self: []const u8, w: *std.io.Writer) std.io.Writer.Error!void {
+        fn formatMessage(self: []const u8, w: *std.Io.Writer) std.Io.Writer.Error!void {
             // Just writes the string for now, but we're keeping this behind a formatter so we have
             // the option to extend it in the future to print more advanced messages (like `Error`
             // does) without breaking the API.
             try w.writeAll(self);
         }
 
-        pub fn fmtMessage(self: Note, diag: *const Diagnostics) std.fmt.Formatter([]const u8, Note.formatMessage) {
+        pub fn fmtMessage(self: Note, diag: *const Diagnostics) std.fmt.Alt([]const u8, Note.formatMessage) {
             return .{ .data = switch (self) {
                 .zoir => |note| note.msg.get(diag.zoir),
                 .type_check => |note| note.msg,
@@ -147,14 +147,14 @@ pub const Error = union(enum) {
         diag: *const Diagnostics,
     };
 
-    fn formatMessage(self: FormatMessage, w: *std.io.Writer) std.io.Writer.Error!void {
+    fn formatMessage(self: FormatMessage, w: *std.Io.Writer) std.Io.Writer.Error!void {
         switch (self.err) {
             .zoir => |err| try w.writeAll(err.msg.get(self.diag.zoir)),
             .type_check => |tc| try w.writeAll(tc.message),
         }
     }
 
-    pub fn fmtMessage(self: @This(), diag: *const Diagnostics) std.fmt.Formatter(FormatMessage, formatMessage) {
+    pub fn fmtMessage(self: @This(), diag: *const Diagnostics) std.fmt.Alt(FormatMessage, formatMessage) {
         return .{ .data = .{
             .err = self,
             .diag = diag,
@@ -226,7 +226,7 @@ pub const Diagnostics = struct {
         return .{ .diag = self };
     }
 
-    pub fn format(self: *const @This(), w: *std.io.Writer) std.io.Writer.Error!void {
+    pub fn format(self: *const @This(), w: *std.Io.Writer) std.Io.Writer.Error!void {
         var errors = self.iterateErrors();
         while (errors.next()) |err| {
             const loc = err.getLocation(self);
@@ -606,7 +606,7 @@ const Parser = struct {
         }
     }
 
-    fn parseSlicePointer(self: *@This(), T: type, node: Zoir.Node.Index) !T {
+    fn parseSlicePointer(self: *@This(), T: type, node: Zoir.Node.Index) ParseExprInnerError!T {
         switch (node.get(self.zoir)) {
             .string_literal => return self.parseString(T, node),
             .array_literal => |nodes| return self.parseSlice(T, nodes),
@@ -1048,6 +1048,7 @@ const Parser = struct {
         name: []const u8,
     ) error{ OutOfMemory, ParseZon } {
         @branchHint(.cold);
+        const gpa = self.gpa;
         const token = if (field) |f| b: {
             var buf: [2]Ast.Node.Index = undefined;
             const struct_init = self.ast.fullStructInit(&buf, node.getAstNode(self.zoir)).?;
@@ -1065,13 +1066,12 @@ const Parser = struct {
                     };
                 } else b: {
                     const msg = "supported: ";
-                    var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(self.gpa, 64);
-                    defer buf.deinit(self.gpa);
-                    const writer = buf.writer(self.gpa);
-                    try writer.writeAll(msg);
+                    var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(gpa, 64);
+                    defer buf.deinit(gpa);
+                    try buf.appendSlice(gpa, msg);
                     inline for (info.fields, 0..) |field_info, i| {
-                        if (i != 0) try writer.writeAll(", ");
-                        try writer.print("'{f}'", .{std.zig.fmtIdFlags(field_info.name, .{
+                        if (i != 0) try buf.appendSlice(gpa, ", ");
+                        try buf.print(gpa, "'{f}'", .{std.zig.fmtIdFlags(field_info.name, .{
                             .allow_primitive = true,
                             .allow_underscore = true,
                         })});
@@ -1079,7 +1079,7 @@ const Parser = struct {
                     break :b .{
                         .token = token,
                         .offset = 0,
-                        .msg = try buf.toOwnedSlice(self.gpa),
+                        .msg = try buf.toOwnedSlice(gpa),
                         .owned = true,
                     };
                 };
lib/std/zon/stringify.zig
@@ -22,6 +22,7 @@
 
 const std = @import("std");
 const assert = std.debug.assert;
+const Writer = std.Io.Writer;
 
 /// Options for `serialize`.
 pub const SerializeOptions = struct {
@@ -40,15 +41,12 @@ pub const SerializeOptions = struct {
 /// Serialize the given value as ZON.
 ///
 /// It is asserted at comptime that `@TypeOf(val)` is not a recursive type.
-pub fn serialize(
-    val: anytype,
-    options: SerializeOptions,
-    writer: anytype,
-) @TypeOf(writer).Error!void {
-    var sz = serializer(writer, .{
-        .whitespace = options.whitespace,
-    });
-    try sz.value(val, .{
+pub fn serialize(val: anytype, options: SerializeOptions, writer: *Writer) Writer.Error!void {
+    var s: Serializer = .{
+        .writer = writer,
+        .options = .{ .whitespace = options.whitespace },
+    };
+    try s.value(val, .{
         .emit_codepoint_literals = options.emit_codepoint_literals,
         .emit_strings_as_containers = options.emit_strings_as_containers,
         .emit_default_optional_fields = options.emit_default_optional_fields,
@@ -62,13 +60,14 @@ pub fn serialize(
 pub fn serializeMaxDepth(
     val: anytype,
     options: SerializeOptions,
-    writer: anytype,
+    writer: *Writer,
     depth: usize,
-) (@TypeOf(writer).Error || error{ExceededMaxDepth})!void {
-    var sz = serializer(writer, .{
-        .whitespace = options.whitespace,
-    });
-    try sz.valueMaxDepth(val, .{
+) Serializer.DepthError!void {
+    var s: Serializer = .{
+        .writer = writer,
+        .options = .{ .whitespace = options.whitespace },
+    };
+    try s.valueMaxDepth(val, .{
         .emit_codepoint_literals = options.emit_codepoint_literals,
         .emit_strings_as_containers = options.emit_strings_as_containers,
         .emit_default_optional_fields = options.emit_default_optional_fields,
@@ -81,44 +80,45 @@ pub fn serializeMaxDepth(
 pub fn serializeArbitraryDepth(
     val: anytype,
     options: SerializeOptions,
-    writer: anytype,
-) @TypeOf(writer).Error!void {
-    var sz = serializer(writer, .{
-        .whitespace = options.whitespace,
-    });
-    try sz.valueArbitraryDepth(val, .{
+    writer: *Writer,
+) Serializer.Error!void {
+    var s: Serializer = .{
+        .writer = writer,
+        .options = .{ .whitespace = options.whitespace },
+    };
+    try s.valueArbitraryDepth(val, .{
         .emit_codepoint_literals = options.emit_codepoint_literals,
         .emit_strings_as_containers = options.emit_strings_as_containers,
         .emit_default_optional_fields = options.emit_default_optional_fields,
     });
 }
 
-fn typeIsRecursive(comptime T: type) bool {
-    return comptime typeIsRecursiveImpl(T, &.{});
+inline fn typeIsRecursive(comptime T: type) bool {
+    return comptime typeIsRecursiveInner(T, &.{});
 }
 
-fn typeIsRecursiveImpl(comptime T: type, comptime prev_visited: []const type) bool {
+fn typeIsRecursiveInner(comptime T: type, comptime prev_visited: []const type) bool {
     for (prev_visited) |V| {
         if (V == T) return true;
     }
     const visited = prev_visited ++ .{T};
 
     return switch (@typeInfo(T)) {
-        .pointer => |pointer| typeIsRecursiveImpl(pointer.child, visited),
-        .optional => |optional| typeIsRecursiveImpl(optional.child, visited),
-        .array => |array| typeIsRecursiveImpl(array.child, visited),
-        .vector => |vector| typeIsRecursiveImpl(vector.child, visited),
+        .pointer => |pointer| typeIsRecursiveInner(pointer.child, visited),
+        .optional => |optional| typeIsRecursiveInner(optional.child, visited),
+        .array => |array| typeIsRecursiveInner(array.child, visited),
+        .vector => |vector| typeIsRecursiveInner(vector.child, visited),
         .@"struct" => |@"struct"| for (@"struct".fields) |field| {
-            if (typeIsRecursiveImpl(field.type, visited)) break true;
+            if (typeIsRecursiveInner(field.type, visited)) break true;
         } else false,
         .@"union" => |@"union"| inline for (@"union".fields) |field| {
-            if (typeIsRecursiveImpl(field.type, visited)) break true;
+            if (typeIsRecursiveInner(field.type, visited)) break true;
         } else false,
         else => false,
     };
 }
 
-fn canSerializeType(T: type) bool {
+inline fn canSerializeType(T: type) bool {
     comptime return canSerializeTypeInner(T, &.{}, false);
 }
 
@@ -343,12 +343,6 @@ test "std.zon checkValueDepth" {
     try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }}));
 }
 
-/// Options for `Serializer`.
-pub const SerializerOptions = struct {
-    /// If false, only syntactically necessary whitespace is emitted.
-    whitespace: bool = true,
-};
-
 /// Determines when to emit Unicode code point literals as opposed to integer literals.
 pub const EmitCodepointLiterals = enum {
     /// Never emit Unicode code point literals.
@@ -440,634 +434,616 @@ pub const SerializeContainerOptions = struct {
 /// For manual serialization of containers, see:
 /// * `beginStruct`
 /// * `beginTuple`
-///
-/// # Example
-/// ```zig
-/// var sz = serializer(writer, .{});
-/// var vec2 = try sz.beginStruct(.{});
-/// try vec2.field("x", 1.5, .{});
-/// try vec2.fieldPrefix();
-/// try sz.value(2.5);
-/// try vec2.end();
-/// ```
-pub fn Serializer(Writer: type) type {
-    return struct {
-        const Self = @This();
-
-        options: SerializerOptions,
-        indent_level: u8,
-        writer: Writer,
-
-        /// Initialize a serializer.
-        fn init(writer: Writer, options: SerializerOptions) Self {
-            return .{
-                .options = options,
-                .writer = writer,
-                .indent_level = 0,
-            };
-        }
+pub const Serializer = struct {
+    options: Options = .{},
+    indent_level: u8 = 0,
+    writer: *Writer,
 
-        /// Serialize a value, similar to `serialize`.
-        pub fn value(self: *Self, val: anytype, options: ValueOptions) Writer.Error!void {
-            comptime assert(!typeIsRecursive(@TypeOf(val)));
-            return self.valueArbitraryDepth(val, options);
-        }
+    pub const Error = Writer.Error;
+    pub const DepthError = Error || error{ExceededMaxDepth};
 
-        /// Serialize a value, similar to `serializeMaxDepth`.
-        pub fn valueMaxDepth(
-            self: *Self,
-            val: anytype,
-            options: ValueOptions,
-            depth: usize,
-        ) (Writer.Error || error{ExceededMaxDepth})!void {
-            try checkValueDepth(val, depth);
-            return self.valueArbitraryDepth(val, options);
-        }
+    pub const Options = struct {
+        /// If false, only syntactically necessary whitespace is emitted.
+        whitespace: bool = true,
+    };
 
-        /// Serialize a value, similar to `serializeArbitraryDepth`.
-        pub fn valueArbitraryDepth(
-            self: *Self,
-            val: anytype,
-            options: ValueOptions,
-        ) Writer.Error!void {
-            comptime assert(canSerializeType(@TypeOf(val)));
-            switch (@typeInfo(@TypeOf(val))) {
-                .int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| {
-                    self.codePoint(c) catch |err| switch (err) {
-                        error.InvalidCodepoint => unreachable, // Already validated
-                        else => |e| return e,
-                    };
-                } else {
-                    try self.int(val);
-                },
-                .float, .comptime_float => try self.float(val),
-                .bool, .null => try std.fmt.format(self.writer, "{}", .{val}),
-                .enum_literal => try self.ident(@tagName(val)),
-                .@"enum" => try self.ident(@tagName(val)),
-                .pointer => |pointer| {
-                    // Try to serialize as a string
-                    const item: ?type = switch (@typeInfo(pointer.child)) {
-                        .array => |array| array.child,
-                        else => if (pointer.size == .slice) pointer.child else null,
-                    };
-                    if (item == u8 and
-                        (pointer.sentinel() == null or pointer.sentinel() == 0) and
-                        !options.emit_strings_as_containers)
-                    {
-                        return try self.string(val);
-                    }
+    /// Serialize a value, similar to `serialize`.
+    pub fn value(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
+        comptime assert(!typeIsRecursive(@TypeOf(val)));
+        return self.valueArbitraryDepth(val, options);
+    }
 
-                    // Serialize as either a tuple or as the child type
-                    switch (pointer.size) {
-                        .slice => try self.tupleImpl(val, options),
-                        .one => try self.valueArbitraryDepth(val.*, options),
-                        else => comptime unreachable,
-                    }
-                },
-                .array => {
-                    var container = try self.beginTuple(
-                        .{ .whitespace_style = .{ .fields = val.len } },
-                    );
-                    for (val) |item_val| {
-                        try container.fieldArbitraryDepth(item_val, options);
-                    }
-                    try container.end();
-                },
-                .@"struct" => |@"struct"| if (@"struct".is_tuple) {
-                    var container = try self.beginTuple(
-                        .{ .whitespace_style = .{ .fields = @"struct".fields.len } },
-                    );
-                    inline for (val) |field_value| {
-                        try container.fieldArbitraryDepth(field_value, options);
-                    }
-                    try container.end();
-                } else {
-                    // Decide which fields to emit
-                    const fields, const skipped: [@"struct".fields.len]bool = if (options.emit_default_optional_fields) b: {
-                        break :b .{ @"struct".fields.len, @splat(false) };
-                    } else b: {
-                        var fields = @"struct".fields.len;
-                        var skipped: [@"struct".fields.len]bool = @splat(false);
-                        inline for (@"struct".fields, &skipped) |field_info, *skip| {
-                            if (field_info.default_value_ptr) |ptr| {
-                                const default: *const field_info.type = @ptrCast(@alignCast(ptr));
-                                const field_value = @field(val, field_info.name);
-                                if (std.meta.eql(field_value, default.*)) {
-                                    skip.* = true;
-                                    fields -= 1;
-                                }
+    /// Serialize a value, similar to `serializeMaxDepth`.
+    /// Can return `error.ExceededMaxDepth`.
+    pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, depth: usize) DepthError!void {
+        try checkValueDepth(val, depth);
+        return self.valueArbitraryDepth(val, options);
+    }
+
+    /// Serialize a value, similar to `serializeArbitraryDepth`.
+    pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
+        comptime assert(canSerializeType(@TypeOf(val)));
+        switch (@typeInfo(@TypeOf(val))) {
+            .int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| {
+                self.codePoint(c) catch |err| switch (err) {
+                    error.InvalidCodepoint => unreachable, // Already validated
+                    else => |e| return e,
+                };
+            } else {
+                try self.int(val);
+            },
+            .float, .comptime_float => try self.float(val),
+            .bool, .null => try self.writer.print("{}", .{val}),
+            .enum_literal => try self.ident(@tagName(val)),
+            .@"enum" => try self.ident(@tagName(val)),
+            .pointer => |pointer| {
+                // Try to serialize as a string
+                const item: ?type = switch (@typeInfo(pointer.child)) {
+                    .array => |array| array.child,
+                    else => if (pointer.size == .slice) pointer.child else null,
+                };
+                if (item == u8 and
+                    (pointer.sentinel() == null or pointer.sentinel() == 0) and
+                    !options.emit_strings_as_containers)
+                {
+                    return try self.string(val);
+                }
+
+                // Serialize as either a tuple or as the child type
+                switch (pointer.size) {
+                    .slice => try self.tupleImpl(val, options),
+                    .one => try self.valueArbitraryDepth(val.*, options),
+                    else => comptime unreachable,
+                }
+            },
+            .array => {
+                var container = try self.beginTuple(
+                    .{ .whitespace_style = .{ .fields = val.len } },
+                );
+                for (val) |item_val| {
+                    try container.fieldArbitraryDepth(item_val, options);
+                }
+                try container.end();
+            },
+            .@"struct" => |@"struct"| if (@"struct".is_tuple) {
+                var container = try self.beginTuple(
+                    .{ .whitespace_style = .{ .fields = @"struct".fields.len } },
+                );
+                inline for (val) |field_value| {
+                    try container.fieldArbitraryDepth(field_value, options);
+                }
+                try container.end();
+            } else {
+                // Decide which fields to emit
+                const fields, const skipped: [@"struct".fields.len]bool = if (options.emit_default_optional_fields) b: {
+                    break :b .{ @"struct".fields.len, @splat(false) };
+                } else b: {
+                    var fields = @"struct".fields.len;
+                    var skipped: [@"struct".fields.len]bool = @splat(false);
+                    inline for (@"struct".fields, &skipped) |field_info, *skip| {
+                        if (field_info.default_value_ptr) |ptr| {
+                            const default: *const field_info.type = @ptrCast(@alignCast(ptr));
+                            const field_value = @field(val, field_info.name);
+                            if (std.meta.eql(field_value, default.*)) {
+                                skip.* = true;
+                                fields -= 1;
                             }
                         }
-                        break :b .{ fields, skipped };
-                    };
-
-                    // Emit those fields
-                    var container = try self.beginStruct(
-                        .{ .whitespace_style = .{ .fields = fields } },
-                    );
-                    inline for (@"struct".fields, skipped) |field_info, skip| {
-                        if (!skip) {
-                            try container.fieldArbitraryDepth(
-                                field_info.name,
-                                @field(val, field_info.name),
-                                options,
-                            );
-                        }
-                    }
-                    try container.end();
-                },
-                .@"union" => |@"union"| {
-                    comptime assert(@"union".tag_type != null);
-                    switch (val) {
-                        inline else => |pl, tag| if (@TypeOf(pl) == void)
-                            try self.writer.print(".{s}", .{@tagName(tag)})
-                        else {
-                            var container = try self.beginStruct(.{ .whitespace_style = .{ .fields = 1 } });
-
-                            try container.fieldArbitraryDepth(
-                                @tagName(tag),
-                                pl,
-                                options,
-                            );
-
-                            try container.end();
-                        },
                     }
-                },
-                .optional => if (val) |inner| {
-                    try self.valueArbitraryDepth(inner, options);
-                } else {
-                    try self.writer.writeAll("null");
-                },
-                .vector => |vector| {
-                    var container = try self.beginTuple(
-                        .{ .whitespace_style = .{ .fields = vector.len } },
-                    );
-                    for (0..vector.len) |i| {
-                        try container.fieldArbitraryDepth(val[i], options);
+                    break :b .{ fields, skipped };
+                };
+
+                // Emit those fields
+                var container = try self.beginStruct(
+                    .{ .whitespace_style = .{ .fields = fields } },
+                );
+                inline for (@"struct".fields, skipped) |field_info, skip| {
+                    if (!skip) {
+                        try container.fieldArbitraryDepth(
+                            field_info.name,
+                            @field(val, field_info.name),
+                            options,
+                        );
                     }
-                    try container.end();
-                },
+                }
+                try container.end();
+            },
+            .@"union" => |@"union"| {
+                comptime assert(@"union".tag_type != null);
+                switch (val) {
+                    inline else => |pl, tag| if (@TypeOf(pl) == void)
+                        try self.writer.print(".{s}", .{@tagName(tag)})
+                    else {
+                        var container = try self.beginStruct(.{ .whitespace_style = .{ .fields = 1 } });
+
+                        try container.fieldArbitraryDepth(
+                            @tagName(tag),
+                            pl,
+                            options,
+                        );
+
+                        try container.end();
+                    },
+                }
+            },
+            .optional => if (val) |inner| {
+                try self.valueArbitraryDepth(inner, options);
+            } else {
+                try self.writer.writeAll("null");
+            },
+            .vector => |vector| {
+                var container = try self.beginTuple(
+                    .{ .whitespace_style = .{ .fields = vector.len } },
+                );
+                for (0..vector.len) |i| {
+                    try container.fieldArbitraryDepth(val[i], options);
+                }
+                try container.end();
+            },
 
-                else => comptime unreachable,
-            }
+            else => comptime unreachable,
         }
+    }
 
-        /// Serialize an integer.
-        pub fn int(self: *Self, val: anytype) Writer.Error!void {
-            //try self.writer.printInt(val, 10, .lower, .{});
-            try std.fmt.format(self.writer, "{d}", .{val});
-        }
-
-        /// Serialize a float.
-        pub fn float(self: *Self, val: anytype) Writer.Error!void {
-            switch (@typeInfo(@TypeOf(val))) {
-                .float => if (std.math.isNan(val)) {
-                    return self.writer.writeAll("nan");
-                } else if (std.math.isPositiveInf(val)) {
-                    return self.writer.writeAll("inf");
-                } else if (std.math.isNegativeInf(val)) {
-                    return self.writer.writeAll("-inf");
-                } else if (std.math.isNegativeZero(val)) {
-                    return self.writer.writeAll("-0.0");
-                } else {
-                    try std.fmt.format(self.writer, "{d}", .{val});
-                },
-                .comptime_float => if (val == 0) {
-                    return self.writer.writeAll("0");
-                } else {
-                    try std.fmt.format(self.writer, "{d}", .{val});
-                },
-                else => comptime unreachable,
-            }
-        }
+    /// Serialize an integer.
+    pub fn int(self: *Serializer, val: anytype) Error!void {
+        try self.writer.printInt(val, 10, .lower, .{});
+    }
 
-        /// Serialize `name` as an identifier prefixed with `.`.
-        ///
-        /// Escapes the identifier if necessary.
-        pub fn ident(self: *Self, name: []const u8) Writer.Error!void {
-            try self.writer.print(".{f}", .{std.zig.fmtIdPU(name)});
+    /// Serialize a float.
+    pub fn float(self: *Serializer, val: anytype) Error!void {
+        switch (@typeInfo(@TypeOf(val))) {
+            .float => if (std.math.isNan(val)) {
+                return self.writer.writeAll("nan");
+            } else if (std.math.isPositiveInf(val)) {
+                return self.writer.writeAll("inf");
+            } else if (std.math.isNegativeInf(val)) {
+                return self.writer.writeAll("-inf");
+            } else if (std.math.isNegativeZero(val)) {
+                return self.writer.writeAll("-0.0");
+            } else {
+                try self.writer.print("{d}", .{val});
+            },
+            .comptime_float => if (val == 0) {
+                return self.writer.writeAll("0");
+            } else {
+                try self.writer.print("{d}", .{val});
+            },
+            else => comptime unreachable,
         }
+    }
 
-        /// Serialize `val` as a Unicode codepoint.
-        ///
-        /// Returns `error.InvalidCodepoint` if `val` is not a valid Unicode codepoint.
-        pub fn codePoint(
-            self: *Self,
-            val: u21,
-        ) (Writer.Error || error{InvalidCodepoint})!void {
-            var buf: [8]u8 = undefined;
-            const len = std.unicode.utf8Encode(val, &buf) catch return error.InvalidCodepoint;
-            const str = buf[0..len];
-            try std.fmt.format(self.writer, "'{f}'", .{std.zig.fmtChar(str)});
-        }
-
-        /// Like `value`, but always serializes `val` as a tuple.
-        ///
-        /// Will fail at comptime if `val` is not a tuple, array, pointer to an array, or slice.
-        pub fn tuple(self: *Self, val: anytype, options: ValueOptions) Writer.Error!void {
-            comptime assert(!typeIsRecursive(@TypeOf(val)));
-            try self.tupleArbitraryDepth(val, options);
-        }
+    /// Serialize `name` as an identifier prefixed with `.`.
+    ///
+    /// Escapes the identifier if necessary.
+    pub fn ident(self: *Serializer, name: []const u8) Error!void {
+        try self.writer.print(".{f}", .{std.zig.fmtIdPU(name)});
+    }
 
-        /// Like `tuple`, but recursive types are allowed.
-        ///
-        /// Returns `error.ExceededMaxDepth` if `depth` is exceeded.
-        pub fn tupleMaxDepth(
-            self: *Self,
-            val: anytype,
-            options: ValueOptions,
-            depth: usize,
-        ) (Writer.Error || error{ExceededMaxDepth})!void {
-            try checkValueDepth(val, depth);
-            try self.tupleArbitraryDepth(val, options);
-        }
+    pub const CodePointError = Error || error{InvalidCodepoint};
 
-        /// Like `tuple`, but recursive types are allowed.
-        ///
-        /// It is the caller's responsibility to ensure that `val` does not contain cycles.
-        pub fn tupleArbitraryDepth(
-            self: *Self,
-            val: anytype,
-            options: ValueOptions,
-        ) Writer.Error!void {
-            try self.tupleImpl(val, options);
-        }
+    /// Serialize `val` as a Unicode codepoint.
+    ///
+    /// Returns `error.InvalidCodepoint` if `val` is not a valid Unicode codepoint.
+    pub fn codePoint(self: *Serializer, val: u21) CodePointError!void {
+        try self.writer.print("'{f}'", .{std.zig.fmtChar(val)});
+    }
 
-        fn tupleImpl(self: *Self, val: anytype, options: ValueOptions) Writer.Error!void {
-            comptime assert(canSerializeType(@TypeOf(val)));
-            switch (@typeInfo(@TypeOf(val))) {
-                .@"struct" => {
-                    var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } });
-                    inline for (val) |item_val| {
-                        try container.fieldArbitraryDepth(item_val, options);
-                    }
-                    try container.end();
-                },
-                .pointer, .array => {
-                    var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } });
-                    for (val) |item_val| {
-                        try container.fieldArbitraryDepth(item_val, options);
-                    }
-                    try container.end();
-                },
-                else => comptime unreachable,
-            }
-        }
+    /// Like `value`, but always serializes `val` as a tuple.
+    ///
+    /// Will fail at comptime if `val` is not a tuple, array, pointer to an array, or slice.
+    pub fn tuple(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
+        comptime assert(!typeIsRecursive(@TypeOf(val)));
+        try self.tupleArbitraryDepth(val, options);
+    }
 
-        /// Like `value`, but always serializes `val` as a string.
-        pub fn string(self: *Self, val: []const u8) Writer.Error!void {
-            try std.fmt.format(self.writer, "\"{f}\"", .{std.zig.fmtString(val)});
-        }
+    /// Like `tuple`, but recursive types are allowed.
+    ///
+    /// Returns `error.ExceededMaxDepth` if `depth` is exceeded.
+    pub fn tupleMaxDepth(
+        self: *Serializer,
+        val: anytype,
+        options: ValueOptions,
+        depth: usize,
+    ) DepthError!void {
+        try checkValueDepth(val, depth);
+        try self.tupleArbitraryDepth(val, options);
+    }
 
-        /// Options for formatting multiline strings.
-        pub const MultilineStringOptions = struct {
-            /// If top level is true, whitespace before and after the multiline string is elided.
-            /// If it is true, a newline is printed, then the value, followed by a newline, and if
-            /// whitespace is true any necessary indentation follows.
-            top_level: bool = false,
-        };
+    /// Like `tuple`, but recursive types are allowed.
+    ///
+    /// It is the caller's responsibility to ensure that `val` does not contain cycles.
+    pub fn tupleArbitraryDepth(
+        self: *Serializer,
+        val: anytype,
+        options: ValueOptions,
+    ) Error!void {
+        try self.tupleImpl(val, options);
+    }
 
-        /// Like `value`, but always serializes to a multiline string literal.
-        ///
-        /// Returns `error.InnerCarriageReturn` if `val` contains a CR not followed by a newline,
-        /// since multiline strings cannot represent CR without a following newline.
-        pub fn multilineString(
-            self: *Self,
-            val: []const u8,
-            options: MultilineStringOptions,
-        ) (Writer.Error || error{InnerCarriageReturn})!void {
-            // Make sure the string does not contain any carriage returns not followed by a newline
-            var i: usize = 0;
-            while (i < val.len) : (i += 1) {
-                if (val[i] == '\r') {
-                    if (i + 1 < val.len) {
-                        if (val[i + 1] == '\n') {
-                            i += 1;
-                            continue;
-                        }
-                    }
-                    return error.InnerCarriageReturn;
+    fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
+        comptime assert(canSerializeType(@TypeOf(val)));
+        switch (@typeInfo(@TypeOf(val))) {
+            .@"struct" => {
+                var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } });
+                inline for (val) |item_val| {
+                    try container.fieldArbitraryDepth(item_val, options);
                 }
-            }
+                try container.end();
+            },
+            .pointer, .array => {
+                var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } });
+                for (val) |item_val| {
+                    try container.fieldArbitraryDepth(item_val, options);
+                }
+                try container.end();
+            },
+            else => comptime unreachable,
+        }
+    }
 
-            if (!options.top_level) {
-                try self.newline();
-                try self.indent();
-            }
+    /// Like `value`, but always serializes `val` as a string.
+    pub fn string(self: *Serializer, val: []const u8) Error!void {
+        try self.writer.print("\"{f}\"", .{std.zig.fmtString(val)});
+    }
+
+    /// Options for formatting multiline strings.
+    pub const MultilineStringOptions = struct {
+        /// If top level is true, whitespace before and after the multiline string is elided.
+        /// If it is true, a newline is printed, then the value, followed by a newline, and if
+        /// whitespace is true any necessary indentation follows.
+        top_level: bool = false,
+    };
 
-            try self.writer.writeAll("\\\\");
-            for (val) |c| {
-                if (c != '\r') {
-                    try self.writer.writeByte(c); // We write newlines here even if whitespace off
-                    if (c == '\n') {
-                        try self.indent();
-                        try self.writer.writeAll("\\\\");
+    pub const MultilineStringError = Error || error{InnerCarriageReturn};
+
+    /// Like `value`, but always serializes to a multiline string literal.
+    ///
+    /// Returns `error.InnerCarriageReturn` if `val` contains a CR not followed by a newline,
+    /// since multiline strings cannot represent CR without a following newline.
+    pub fn multilineString(
+        self: *Serializer,
+        val: []const u8,
+        options: MultilineStringOptions,
+    ) MultilineStringError!void {
+        // Make sure the string does not contain any carriage returns not followed by a newline
+        var i: usize = 0;
+        while (i < val.len) : (i += 1) {
+            if (val[i] == '\r') {
+                if (i + 1 < val.len) {
+                    if (val[i + 1] == '\n') {
+                        i += 1;
+                        continue;
                     }
                 }
+                return error.InnerCarriageReturn;
             }
+        }
 
-            if (!options.top_level) {
-                try self.writer.writeByte('\n'); // Even if whitespace off
-                try self.indent();
-            }
+        if (!options.top_level) {
+            try self.newline();
+            try self.indent();
         }
 
-        /// Create a `Struct` for writing ZON structs field by field.
-        pub fn beginStruct(
-            self: *Self,
-            options: SerializeContainerOptions,
-        ) Writer.Error!Struct {
-            return Struct.begin(self, options);
+        try self.writer.writeAll("\\\\");
+        for (val) |c| {
+            if (c != '\r') {
+                try self.writer.writeByte(c); // We write newlines here even if whitespace off
+                if (c == '\n') {
+                    try self.indent();
+                    try self.writer.writeAll("\\\\");
+                }
+            }
         }
 
-        /// Creates a `Tuple` for writing ZON tuples field by field.
-        pub fn beginTuple(
-            self: *Self,
-            options: SerializeContainerOptions,
-        ) Writer.Error!Tuple {
-            return Tuple.begin(self, options);
+        if (!options.top_level) {
+            try self.writer.writeByte('\n'); // Even if whitespace off
+            try self.indent();
         }
+    }
 
-        fn indent(self: *Self) Writer.Error!void {
-            if (self.options.whitespace) {
-                try self.writer.writeByteNTimes(' ', 4 * self.indent_level);
-            }
+    /// Create a `Struct` for writing ZON structs field by field.
+    pub fn beginStruct(
+        self: *Serializer,
+        options: SerializeContainerOptions,
+    ) Error!Struct {
+        return Struct.begin(self, options);
+    }
+
+    /// Creates a `Tuple` for writing ZON tuples field by field.
+    pub fn beginTuple(
+        self: *Serializer,
+        options: SerializeContainerOptions,
+    ) Error!Tuple {
+        return Tuple.begin(self, options);
+    }
+
+    fn indent(self: *Serializer) Error!void {
+        if (self.options.whitespace) {
+            try self.writer.splatByteAll(' ', 4 * self.indent_level);
         }
+    }
 
-        fn newline(self: *Self) Writer.Error!void {
-            if (self.options.whitespace) {
-                try self.writer.writeByte('\n');
-            }
+    fn newline(self: *Serializer) Error!void {
+        if (self.options.whitespace) {
+            try self.writer.writeByte('\n');
         }
+    }
 
-        fn newlineOrSpace(self: *Self, len: usize) Writer.Error!void {
-            if (self.containerShouldWrap(len)) {
-                try self.newline();
-            } else {
-                try self.space();
-            }
+    fn newlineOrSpace(self: *Serializer, len: usize) Error!void {
+        if (self.containerShouldWrap(len)) {
+            try self.newline();
+        } else {
+            try self.space();
         }
+    }
 
-        fn space(self: *Self) Writer.Error!void {
-            if (self.options.whitespace) {
-                try self.writer.writeByte(' ');
-            }
+    fn space(self: *Serializer) Error!void {
+        if (self.options.whitespace) {
+            try self.writer.writeByte(' ');
         }
+    }
 
-        /// Writes ZON tuples field by field.
-        pub const Tuple = struct {
-            container: Container,
+    /// Writes ZON tuples field by field.
+    pub const Tuple = struct {
+        container: Container,
 
-            fn begin(parent: *Self, options: SerializeContainerOptions) Writer.Error!Tuple {
-                return .{
-                    .container = try Container.begin(parent, .anon, options),
-                };
-            }
+        fn begin(parent: *Serializer, options: SerializeContainerOptions) Error!Tuple {
+            return .{
+                .container = try Container.begin(parent, .anon, options),
+            };
+        }
 
-            /// Finishes serializing the tuple.
-            ///
-            /// Prints a trailing comma as configured when appropriate, and the closing bracket.
-            pub fn end(self: *Tuple) Writer.Error!void {
-                try self.container.end();
-                self.* = undefined;
-            }
+        /// Finishes serializing the tuple.
+        ///
+        /// Prints a trailing comma as configured when appropriate, and the closing bracket.
+        pub fn end(self: *Tuple) Error!void {
+            try self.container.end();
+            self.* = undefined;
+        }
 
-            /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`.
-            pub fn field(
-                self: *Tuple,
-                val: anytype,
-                options: ValueOptions,
-            ) Writer.Error!void {
-                try self.container.field(null, val, options);
-            }
+        /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`.
+        pub fn field(
+            self: *Tuple,
+            val: anytype,
+            options: ValueOptions,
+        ) Error!void {
+            try self.container.field(null, val, options);
+        }
 
-            /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`.
-            pub fn fieldMaxDepth(
-                self: *Tuple,
-                val: anytype,
-                options: ValueOptions,
-                depth: usize,
-            ) (Writer.Error || error{ExceededMaxDepth})!void {
-                try self.container.fieldMaxDepth(null, val, options, depth);
-            }
+        /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`.
+        /// Returns `error.ExceededMaxDepth` if `depth` is exceeded.
+        pub fn fieldMaxDepth(
+            self: *Tuple,
+            val: anytype,
+            options: ValueOptions,
+            depth: usize,
+        ) DepthError!void {
+            try self.container.fieldMaxDepth(null, val, options, depth);
+        }
 
-            /// Serialize a field. Equivalent to calling `fieldPrefix` followed by
-            /// `valueArbitraryDepth`.
-            pub fn fieldArbitraryDepth(
-                self: *Tuple,
-                val: anytype,
-                options: ValueOptions,
-            ) Writer.Error!void {
-                try self.container.fieldArbitraryDepth(null, val, options);
-            }
+        /// Serialize a field. Equivalent to calling `fieldPrefix` followed by
+        /// `valueArbitraryDepth`.
+        pub fn fieldArbitraryDepth(
+            self: *Tuple,
+            val: anytype,
+            options: ValueOptions,
+        ) Error!void {
+            try self.container.fieldArbitraryDepth(null, val, options);
+        }
 
-            /// Starts a field with a struct as a value. Returns the struct.
-            pub fn beginStructField(
-                self: *Tuple,
-                options: SerializeContainerOptions,
-            ) Writer.Error!Struct {
-                try self.fieldPrefix();
-                return self.container.serializer.beginStruct(options);
-            }
+        /// Starts a field with a struct as a value. Returns the struct.
+        pub fn beginStructField(
+            self: *Tuple,
+            options: SerializeContainerOptions,
+        ) Error!Struct {
+            try self.fieldPrefix();
+            return self.container.serializer.beginStruct(options);
+        }
 
-            /// Starts a field with a tuple as a value. Returns the tuple.
-            pub fn beginTupleField(
-                self: *Tuple,
-                options: SerializeContainerOptions,
-            ) Writer.Error!Tuple {
-                try self.fieldPrefix();
-                return self.container.serializer.beginTuple(options);
-            }
+        /// Starts a field with a tuple as a value. Returns the tuple.
+        pub fn beginTupleField(
+            self: *Tuple,
+            options: SerializeContainerOptions,
+        ) Error!Tuple {
+            try self.fieldPrefix();
+            return self.container.serializer.beginTuple(options);
+        }
 
-            /// Print a field prefix. This prints any necessary commas, and whitespace as
-            /// configured. Useful if you want to serialize the field value yourself.
-            pub fn fieldPrefix(self: *Tuple) Writer.Error!void {
-                try self.container.fieldPrefix(null);
-            }
-        };
+        /// Print a field prefix. This prints any necessary commas, and whitespace as
+        /// configured. Useful if you want to serialize the field value yourself.
+        pub fn fieldPrefix(self: *Tuple) Error!void {
+            try self.container.fieldPrefix(null);
+        }
+    };
 
-        /// Writes ZON structs field by field.
-        pub const Struct = struct {
-            container: Container,
+    /// Writes ZON structs field by field.
+    pub const Struct = struct {
+        container: Container,
 
-            fn begin(parent: *Self, options: SerializeContainerOptions) Writer.Error!Struct {
-                return .{
-                    .container = try Container.begin(parent, .named, options),
-                };
-            }
+        fn begin(parent: *Serializer, options: SerializeContainerOptions) Error!Struct {
+            return .{
+                .container = try Container.begin(parent, .named, options),
+            };
+        }
 
-            /// Finishes serializing the struct.
-            ///
-            /// Prints a trailing comma as configured when appropriate, and the closing bracket.
-            pub fn end(self: *Struct) Writer.Error!void {
-                try self.container.end();
-                self.* = undefined;
-            }
+        /// Finishes serializing the struct.
+        ///
+        /// Prints a trailing comma as configured when appropriate, and the closing bracket.
+        pub fn end(self: *Struct) Error!void {
+            try self.container.end();
+            self.* = undefined;
+        }
 
-            /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`.
-            pub fn field(
-                self: *Struct,
-                name: []const u8,
-                val: anytype,
-                options: ValueOptions,
-            ) Writer.Error!void {
-                try self.container.field(name, val, options);
-            }
+        /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `value`.
+        pub fn field(
+            self: *Struct,
+            name: []const u8,
+            val: anytype,
+            options: ValueOptions,
+        ) Error!void {
+            try self.container.field(name, val, options);
+        }
 
-            /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`.
-            pub fn fieldMaxDepth(
-                self: *Struct,
-                name: []const u8,
-                val: anytype,
-                options: ValueOptions,
-                depth: usize,
-            ) (Writer.Error || error{ExceededMaxDepth})!void {
-                try self.container.fieldMaxDepth(name, val, options, depth);
-            }
+        /// Serialize a field. Equivalent to calling `fieldPrefix` followed by `valueMaxDepth`.
+        /// Returns `error.ExceededMaxDepth` if `depth` is exceeded.
+        pub fn fieldMaxDepth(
+            self: *Struct,
+            name: []const u8,
+            val: anytype,
+            options: ValueOptions,
+            depth: usize,
+        ) DepthError!void {
+            try self.container.fieldMaxDepth(name, val, options, depth);
+        }
 
-            /// Serialize a field. Equivalent to calling `fieldPrefix` followed by
-            /// `valueArbitraryDepth`.
-            pub fn fieldArbitraryDepth(
-                self: *Struct,
-                name: []const u8,
-                val: anytype,
-                options: ValueOptions,
-            ) Writer.Error!void {
-                try self.container.fieldArbitraryDepth(name, val, options);
-            }
+        /// Serialize a field. Equivalent to calling `fieldPrefix` followed by
+        /// `valueArbitraryDepth`.
+        pub fn fieldArbitraryDepth(
+            self: *Struct,
+            name: []const u8,
+            val: anytype,
+            options: ValueOptions,
+        ) Error!void {
+            try self.container.fieldArbitraryDepth(name, val, options);
+        }
 
-            /// Starts a field with a struct as a value. Returns the struct.
-            pub fn beginStructField(
-                self: *Struct,
-                name: []const u8,
-                options: SerializeContainerOptions,
-            ) Writer.Error!Struct {
-                try self.fieldPrefix(name);
-                return self.container.serializer.beginStruct(options);
-            }
+        /// Starts a field with a struct as a value. Returns the struct.
+        pub fn beginStructField(
+            self: *Struct,
+            name: []const u8,
+            options: SerializeContainerOptions,
+        ) Error!Struct {
+            try self.fieldPrefix(name);
+            return self.container.serializer.beginStruct(options);
+        }
 
-            /// Starts a field with a tuple as a value. Returns the tuple.
-            pub fn beginTupleField(
-                self: *Struct,
-                name: []const u8,
-                options: SerializeContainerOptions,
-            ) Writer.Error!Tuple {
-                try self.fieldPrefix(name);
-                return self.container.serializer.beginTuple(options);
-            }
+        /// Starts a field with a tuple as a value. Returns the tuple.
+        pub fn beginTupleField(
+            self: *Struct,
+            name: []const u8,
+            options: SerializeContainerOptions,
+        ) Error!Tuple {
+            try self.fieldPrefix(name);
+            return self.container.serializer.beginTuple(options);
+        }
 
-            /// Print a field prefix. This prints any necessary commas, the field name (escaped if
-            /// necessary) and whitespace as configured. Useful if you want to serialize the field
-            /// value yourself.
-            pub fn fieldPrefix(self: *Struct, name: []const u8) Writer.Error!void {
-                try self.container.fieldPrefix(name);
-            }
-        };
+        /// Print a field prefix. This prints any necessary commas, the field name (escaped if
+        /// necessary) and whitespace as configured. Useful if you want to serialize the field
+        /// value yourself.
+        pub fn fieldPrefix(self: *Struct, name: []const u8) Error!void {
+            try self.container.fieldPrefix(name);
+        }
+    };
 
-        const Container = struct {
-            const FieldStyle = enum { named, anon };
+    const Container = struct {
+        const FieldStyle = enum { named, anon };
 
-            serializer: *Self,
+        serializer: *Serializer,
+        field_style: FieldStyle,
+        options: SerializeContainerOptions,
+        empty: bool,
+
+        fn begin(
+            sz: *Serializer,
             field_style: FieldStyle,
             options: SerializeContainerOptions,
-            empty: bool,
-
-            fn begin(
-                sz: *Self,
-                field_style: FieldStyle,
-                options: SerializeContainerOptions,
-            ) Writer.Error!Container {
-                if (options.shouldWrap()) sz.indent_level +|= 1;
-                try sz.writer.writeAll(".{");
-                return .{
-                    .serializer = sz,
-                    .field_style = field_style,
-                    .options = options,
-                    .empty = true,
-                };
-            }
-
-            fn end(self: *Container) Writer.Error!void {
-                if (self.options.shouldWrap()) self.serializer.indent_level -|= 1;
-                if (!self.empty) {
-                    if (self.options.shouldWrap()) {
-                        if (self.serializer.options.whitespace) {
-                            try self.serializer.writer.writeByte(',');
-                        }
-                        try self.serializer.newline();
-                        try self.serializer.indent();
-                    } else if (!self.shouldElideSpaces()) {
-                        try self.serializer.space();
-                    }
-                }
-                try self.serializer.writer.writeByte('}');
-                self.* = undefined;
-            }
+        ) Error!Container {
+            if (options.shouldWrap()) sz.indent_level +|= 1;
+            try sz.writer.writeAll(".{");
+            return .{
+                .serializer = sz,
+                .field_style = field_style,
+                .options = options,
+                .empty = true,
+            };
+        }
 
-            fn fieldPrefix(self: *Container, name: ?[]const u8) Writer.Error!void {
-                if (!self.empty) {
-                    try self.serializer.writer.writeByte(',');
-                }
-                self.empty = false;
+        fn end(self: *Container) Error!void {
+            if (self.options.shouldWrap()) self.serializer.indent_level -|= 1;
+            if (!self.empty) {
                 if (self.options.shouldWrap()) {
+                    if (self.serializer.options.whitespace) {
+                        try self.serializer.writer.writeByte(',');
+                    }
                     try self.serializer.newline();
+                    try self.serializer.indent();
                 } else if (!self.shouldElideSpaces()) {
                     try self.serializer.space();
                 }
-                if (self.options.shouldWrap()) try self.serializer.indent();
-                if (name) |n| {
-                    try self.serializer.ident(n);
-                    try self.serializer.space();
-                    try self.serializer.writer.writeByte('=');
-                    try self.serializer.space();
-                }
             }
+            try self.serializer.writer.writeByte('}');
+            self.* = undefined;
+        }
 
-            fn field(
-                self: *Container,
-                name: ?[]const u8,
-                val: anytype,
-                options: ValueOptions,
-            ) Writer.Error!void {
-                comptime assert(!typeIsRecursive(@TypeOf(val)));
-                try self.fieldArbitraryDepth(name, val, options);
+        fn fieldPrefix(self: *Container, name: ?[]const u8) Error!void {
+            if (!self.empty) {
+                try self.serializer.writer.writeByte(',');
             }
-
-            fn fieldMaxDepth(
-                self: *Container,
-                name: ?[]const u8,
-                val: anytype,
-                options: ValueOptions,
-                depth: usize,
-            ) (Writer.Error || error{ExceededMaxDepth})!void {
-                try checkValueDepth(val, depth);
-                try self.fieldArbitraryDepth(name, val, options);
+            self.empty = false;
+            if (self.options.shouldWrap()) {
+                try self.serializer.newline();
+            } else if (!self.shouldElideSpaces()) {
+                try self.serializer.space();
             }
-
-            fn fieldArbitraryDepth(
-                self: *Container,
-                name: ?[]const u8,
-                val: anytype,
-                options: ValueOptions,
-            ) Writer.Error!void {
-                try self.fieldPrefix(name);
-                try self.serializer.valueArbitraryDepth(val, options);
+            if (self.options.shouldWrap()) try self.serializer.indent();
+            if (name) |n| {
+                try self.serializer.ident(n);
+                try self.serializer.space();
+                try self.serializer.writer.writeByte('=');
+                try self.serializer.space();
             }
+        }
 
-            fn shouldElideSpaces(self: *const Container) bool {
-                return switch (self.options.whitespace_style) {
-                    .fields => |fields| self.field_style != .named and fields == 1,
-                    else => false,
-                };
-            }
-        };
+        fn field(
+            self: *Container,
+            name: ?[]const u8,
+            val: anytype,
+            options: ValueOptions,
+        ) Error!void {
+            comptime assert(!typeIsRecursive(@TypeOf(val)));
+            try self.fieldArbitraryDepth(name, val, options);
+        }
+
+        /// Returns `error.ExceededMaxDepth` if `depth` is exceeded.
+        fn fieldMaxDepth(
+            self: *Container,
+            name: ?[]const u8,
+            val: anytype,
+            options: ValueOptions,
+            depth: usize,
+        ) DepthError!void {
+            try checkValueDepth(val, depth);
+            try self.fieldArbitraryDepth(name, val, options);
+        }
+
+        fn fieldArbitraryDepth(
+            self: *Container,
+            name: ?[]const u8,
+            val: anytype,
+            options: ValueOptions,
+        ) Error!void {
+            try self.fieldPrefix(name);
+            try self.serializer.valueArbitraryDepth(val, options);
+        }
+
+        fn shouldElideSpaces(self: *const Container) bool {
+            return switch (self.options.whitespace_style) {
+                .fields => |fields| self.field_style != .named and fields == 1,
+                else => false,
+            };
+        }
     };
-}
+};
 
-/// Creates a new `Serializer` with the given writer and options.
-pub fn serializer(writer: anytype, options: SerializerOptions) Serializer(@TypeOf(writer)) {
-    return .init(writer, options);
+test Serializer {
+    var discarding: Writer.Discarding = .init(&.{});
+    var s: Serializer = .{ .writer = &discarding.writer };
+    var vec2 = try s.beginStruct(.{});
+    try vec2.field("x", 1.5, .{});
+    try vec2.fieldPrefix("prefix");
+    try s.value(2.5, .{});
+    try vec2.end();
 }
 
 fn expectSerializeEqual(
@@ -1075,10 +1051,12 @@ fn expectSerializeEqual(
     value: anytype,
     options: SerializeOptions,
 ) !void {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    try serialize(value, options, buf.writer());
-    try std.testing.expectEqualStrings(expected, buf.items);
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    const bw = &aw.writer;
+    defer aw.deinit();
+
+    try serialize(value, options, bw);
+    try std.testing.expectEqualStrings(expected, aw.getWritten());
 }
 
 test "std.zon stringify whitespace, high level API" {
@@ -1175,59 +1153,59 @@ test "std.zon stringify whitespace, high level API" {
 }
 
 test "std.zon stringify whitespace, low level API" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
-    inline for (.{ true, false }) |whitespace| {
-        sz.options = .{ .whitespace = whitespace };
+    for ([2]bool{ true, false }) |whitespace| {
+        s.options = .{ .whitespace = whitespace };
 
         // Empty containers
         {
-            var container = try sz.beginStruct(.{});
+            var container = try s.beginStruct(.{});
             try container.end();
-            try std.testing.expectEqualStrings(".{}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{});
+            var container = try s.beginTuple(.{});
             try container.end();
-            try std.testing.expectEqualStrings(".{}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
             try container.end();
-            try std.testing.expectEqualStrings(".{}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
             try container.end();
-            try std.testing.expectEqualStrings(".{}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .fields = 0 } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .fields = 0 } });
             try container.end();
-            try std.testing.expectEqualStrings(".{}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .fields = 0 } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .fields = 0 } });
             try container.end();
-            try std.testing.expectEqualStrings(".{}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         // Size 1
         {
-            var container = try sz.beginStruct(.{});
+            var container = try s.beginStruct(.{});
             try container.field("a", 1, .{});
             try container.end();
             if (whitespace) {
@@ -1235,15 +1213,15 @@ test "std.zon stringify whitespace, low level API" {
                     \\.{
                     \\    .a = 1,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{});
+            var container = try s.beginTuple(.{});
             try container.field(1, .{});
             try container.end();
             if (whitespace) {
@@ -1251,62 +1229,62 @@ test "std.zon stringify whitespace, low level API" {
                     \\.{
                     \\    1,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1}", buf.items);
+                try std.testing.expectEqualStrings(".{1}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
             try container.field("a", 1, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ .a = 1 }", buf.items);
+                try std.testing.expectEqualStrings(".{ .a = 1 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
             // We get extra spaces here, since we didn't know up front that there would only be one
             // field.
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
             try container.field(1, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ 1 }", buf.items);
+                try std.testing.expectEqualStrings(".{ 1 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1}", buf.items);
+                try std.testing.expectEqualStrings(".{1}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .fields = 1 } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .fields = 1 } });
             try container.field("a", 1, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ .a = 1 }", buf.items);
+                try std.testing.expectEqualStrings(".{ .a = 1 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .fields = 1 } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .fields = 1 } });
             try container.field(1, .{});
             try container.end();
-            try std.testing.expectEqualStrings(".{1}", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings(".{1}", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         // Size 2
         {
-            var container = try sz.beginStruct(.{});
+            var container = try s.beginStruct(.{});
             try container.field("a", 1, .{});
             try container.field("b", 2, .{});
             try container.end();
@@ -1316,15 +1294,15 @@ test "std.zon stringify whitespace, low level API" {
                     \\    .a = 1,
                     \\    .b = 2,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1,.b=2}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1,.b=2}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{});
+            var container = try s.beginTuple(.{});
             try container.field(1, .{});
             try container.field(2, .{});
             try container.end();
@@ -1334,68 +1312,68 @@ test "std.zon stringify whitespace, low level API" {
                     \\    1,
                     \\    2,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1,2}", buf.items);
+                try std.testing.expectEqualStrings(".{1,2}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
             try container.field("a", 1, .{});
             try container.field("b", 2, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ .a = 1, .b = 2 }", buf.items);
+                try std.testing.expectEqualStrings(".{ .a = 1, .b = 2 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1,.b=2}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1,.b=2}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
             try container.field(1, .{});
             try container.field(2, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ 1, 2 }", buf.items);
+                try std.testing.expectEqualStrings(".{ 1, 2 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1,2}", buf.items);
+                try std.testing.expectEqualStrings(".{1,2}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .fields = 2 } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .fields = 2 } });
             try container.field("a", 1, .{});
             try container.field("b", 2, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ .a = 1, .b = 2 }", buf.items);
+                try std.testing.expectEqualStrings(".{ .a = 1, .b = 2 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1,.b=2}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1,.b=2}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .fields = 2 } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .fields = 2 } });
             try container.field(1, .{});
             try container.field(2, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ 1, 2 }", buf.items);
+                try std.testing.expectEqualStrings(".{ 1, 2 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1,2}", buf.items);
+                try std.testing.expectEqualStrings(".{1,2}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         // Size 3
         {
-            var container = try sz.beginStruct(.{});
+            var container = try s.beginStruct(.{});
             try container.field("a", 1, .{});
             try container.field("b", 2, .{});
             try container.field("c", 3, .{});
@@ -1407,15 +1385,15 @@ test "std.zon stringify whitespace, low level API" {
                     \\    .b = 2,
                     \\    .c = 3,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{});
+            var container = try s.beginTuple(.{});
             try container.field(1, .{});
             try container.field(2, .{});
             try container.field(3, .{});
@@ -1427,43 +1405,43 @@ test "std.zon stringify whitespace, low level API" {
                     \\    2,
                     \\    3,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1,2,3}", buf.items);
+                try std.testing.expectEqualStrings(".{1,2,3}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
             try container.field("a", 1, .{});
             try container.field("b", 2, .{});
             try container.field("c", 3, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ .a = 1, .b = 2, .c = 3 }", buf.items);
+                try std.testing.expectEqualStrings(".{ .a = 1, .b = 2, .c = 3 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .wrap = false } });
             try container.field(1, .{});
             try container.field(2, .{});
             try container.field(3, .{});
             try container.end();
             if (whitespace) {
-                try std.testing.expectEqualStrings(".{ 1, 2, 3 }", buf.items);
+                try std.testing.expectEqualStrings(".{ 1, 2, 3 }", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1,2,3}", buf.items);
+                try std.testing.expectEqualStrings(".{1,2,3}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .fields = 3 } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .fields = 3 } });
             try container.field("a", 1, .{});
             try container.field("b", 2, .{});
             try container.field("c", 3, .{});
@@ -1475,15 +1453,15 @@ test "std.zon stringify whitespace, low level API" {
                     \\    .b = 2,
                     \\    .c = 3,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", buf.items);
+                try std.testing.expectEqualStrings(".{.a=1,.b=2,.c=3}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
-            var container = try sz.beginTuple(.{ .whitespace_style = .{ .fields = 3 } });
+            var container = try s.beginTuple(.{ .whitespace_style = .{ .fields = 3 } });
             try container.field(1, .{});
             try container.field(2, .{});
             try container.field(3, .{});
@@ -1495,16 +1473,16 @@ test "std.zon stringify whitespace, low level API" {
                     \\    2,
                     \\    3,
                     \\}
-                , buf.items);
+                , aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings(".{1,2,3}", buf.items);
+                try std.testing.expectEqualStrings(".{1,2,3}", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         // Nested objects where the outer container doesn't wrap but the inner containers do
         {
-            var container = try sz.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
+            var container = try s.beginStruct(.{ .whitespace_style = .{ .wrap = false } });
             try container.field("first", .{ 1, 2, 3 }, .{});
             try container.field("second", .{ 4, 5, 6 }, .{});
             try container.end();
@@ -1519,139 +1497,141 @@ test "std.zon stringify whitespace, low level API" {
                     \\    5,
                     \\    6,
                     \\} }
-                , buf.items);
+                , aw.getWritten());
             } else {
                 try std.testing.expectEqualStrings(
                     ".{.first=.{1,2,3},.second=.{4,5,6}}",
-                    buf.items,
+                    aw.getWritten(),
                 );
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
     }
 }
 
 test "std.zon stringify utf8 codepoints" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     // Printable ASCII
-    try sz.int('a');
-    try std.testing.expectEqualStrings("97", buf.items);
-    buf.clearRetainingCapacity();
+    try s.int('a');
+    try std.testing.expectEqualStrings("97", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.codePoint('a');
-    try std.testing.expectEqualStrings("'a'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.codePoint('a');
+    try std.testing.expectEqualStrings("'a'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('a', .{ .emit_codepoint_literals = .always });
-    try std.testing.expectEqualStrings("'a'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('a', .{ .emit_codepoint_literals = .always });
+    try std.testing.expectEqualStrings("'a'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('a', .{ .emit_codepoint_literals = .printable_ascii });
-    try std.testing.expectEqualStrings("'a'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('a', .{ .emit_codepoint_literals = .printable_ascii });
+    try std.testing.expectEqualStrings("'a'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('a', .{ .emit_codepoint_literals = .never });
-    try std.testing.expectEqualStrings("97", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('a', .{ .emit_codepoint_literals = .never });
+    try std.testing.expectEqualStrings("97", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Short escaped codepoint
-    try sz.int('\n');
-    try std.testing.expectEqualStrings("10", buf.items);
-    buf.clearRetainingCapacity();
+    try s.int('\n');
+    try std.testing.expectEqualStrings("10", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.codePoint('\n');
-    try std.testing.expectEqualStrings("'\\n'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.codePoint('\n');
+    try std.testing.expectEqualStrings("'\\n'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('\n', .{ .emit_codepoint_literals = .always });
-    try std.testing.expectEqualStrings("'\\n'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('\n', .{ .emit_codepoint_literals = .always });
+    try std.testing.expectEqualStrings("'\\n'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('\n', .{ .emit_codepoint_literals = .printable_ascii });
-    try std.testing.expectEqualStrings("10", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('\n', .{ .emit_codepoint_literals = .printable_ascii });
+    try std.testing.expectEqualStrings("10", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('\n', .{ .emit_codepoint_literals = .never });
-    try std.testing.expectEqualStrings("10", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('\n', .{ .emit_codepoint_literals = .never });
+    try std.testing.expectEqualStrings("10", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Large codepoint
-    try sz.int('⚡');
-    try std.testing.expectEqualStrings("9889", buf.items);
-    buf.clearRetainingCapacity();
+    try s.int('⚡');
+    try std.testing.expectEqualStrings("9889", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.codePoint('⚡');
-    try std.testing.expectEqualStrings("'\\xe2\\x9a\\xa1'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.codePoint('⚡');
+    try std.testing.expectEqualStrings("'\\u{26a1}'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('⚡', .{ .emit_codepoint_literals = .always });
-    try std.testing.expectEqualStrings("'\\xe2\\x9a\\xa1'", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('⚡', .{ .emit_codepoint_literals = .always });
+    try std.testing.expectEqualStrings("'\\u{26a1}'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('⚡', .{ .emit_codepoint_literals = .printable_ascii });
-    try std.testing.expectEqualStrings("9889", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('⚡', .{ .emit_codepoint_literals = .printable_ascii });
+    try std.testing.expectEqualStrings("9889", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value('⚡', .{ .emit_codepoint_literals = .never });
-    try std.testing.expectEqualStrings("9889", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value('⚡', .{ .emit_codepoint_literals = .never });
+    try std.testing.expectEqualStrings("9889", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Invalid codepoint
-    try std.testing.expectError(error.InvalidCodepoint, sz.codePoint(0x110000 + 1));
+    try s.codePoint(0x110000 + 1);
+    try std.testing.expectEqualStrings("'\\u{110001}'", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.int(0x110000 + 1);
-    try std.testing.expectEqualStrings("1114113", buf.items);
-    buf.clearRetainingCapacity();
+    try s.int(0x110000 + 1);
+    try std.testing.expectEqualStrings("1114113", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(0x110000 + 1, .{ .emit_codepoint_literals = .always });
-    try std.testing.expectEqualStrings("1114113", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(0x110000 + 1, .{ .emit_codepoint_literals = .always });
+    try std.testing.expectEqualStrings("1114113", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(0x110000 + 1, .{ .emit_codepoint_literals = .printable_ascii });
-    try std.testing.expectEqualStrings("1114113", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(0x110000 + 1, .{ .emit_codepoint_literals = .printable_ascii });
+    try std.testing.expectEqualStrings("1114113", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(0x110000 + 1, .{ .emit_codepoint_literals = .never });
-    try std.testing.expectEqualStrings("1114113", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(0x110000 + 1, .{ .emit_codepoint_literals = .never });
+    try std.testing.expectEqualStrings("1114113", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Valid codepoint, not a codepoint type
-    try sz.value(@as(u22, 'a'), .{ .emit_codepoint_literals = .always });
-    try std.testing.expectEqualStrings("97", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(@as(u22, 'a'), .{ .emit_codepoint_literals = .always });
+    try std.testing.expectEqualStrings("97", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(@as(u22, 'a'), .{ .emit_codepoint_literals = .printable_ascii });
-    try std.testing.expectEqualStrings("97", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(@as(u22, 'a'), .{ .emit_codepoint_literals = .printable_ascii });
+    try std.testing.expectEqualStrings("97", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(@as(i32, 'a'), .{ .emit_codepoint_literals = .never });
-    try std.testing.expectEqualStrings("97", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(@as(i32, 'a'), .{ .emit_codepoint_literals = .never });
+    try std.testing.expectEqualStrings("97", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Make sure value options are passed to children
-    try sz.value(.{ .c = '⚡' }, .{ .emit_codepoint_literals = .always });
-    try std.testing.expectEqualStrings(".{ .c = '\\xe2\\x9a\\xa1' }", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(.{ .c = '⚡' }, .{ .emit_codepoint_literals = .always });
+    try std.testing.expectEqualStrings(".{ .c = '\\u{26a1}' }", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(.{ .c = '⚡' }, .{ .emit_codepoint_literals = .never });
-    try std.testing.expectEqualStrings(".{ .c = 9889 }", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(.{ .c = '⚡' }, .{ .emit_codepoint_literals = .never });
+    try std.testing.expectEqualStrings(".{ .c = 9889 }", aw.getWritten());
+    aw.clearRetainingCapacity();
 }
 
 test "std.zon stringify strings" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     // Minimal case
-    try sz.string("abc⚡\n");
-    try std.testing.expectEqualStrings("\"abc\\xe2\\x9a\\xa1\\n\"", buf.items);
-    buf.clearRetainingCapacity();
+    try s.string("abc⚡\n");
+    try std.testing.expectEqualStrings("\"abc\\xe2\\x9a\\xa1\\n\"", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.tuple("abc⚡\n", .{});
+    try s.tuple("abc⚡\n", .{});
     try std.testing.expectEqualStrings(
         \\.{
         \\    97,
@@ -1662,14 +1642,14 @@ test "std.zon stringify strings" {
         \\    161,
         \\    10,
         \\}
-    , buf.items);
-    buf.clearRetainingCapacity();
+    , aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value("abc⚡\n", .{});
-    try std.testing.expectEqualStrings("\"abc\\xe2\\x9a\\xa1\\n\"", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value("abc⚡\n", .{});
+    try std.testing.expectEqualStrings("\"abc\\xe2\\x9a\\xa1\\n\"", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value("abc⚡\n", .{ .emit_strings_as_containers = true });
+    try s.value("abc⚡\n", .{ .emit_strings_as_containers = true });
     try std.testing.expectEqualStrings(
         \\.{
         \\    97,
@@ -1680,113 +1660,113 @@ test "std.zon stringify strings" {
         \\    161,
         \\    10,
         \\}
-    , buf.items);
-    buf.clearRetainingCapacity();
+    , aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Value options are inherited by children
-    try sz.value(.{ .str = "abc" }, .{});
-    try std.testing.expectEqualStrings(".{ .str = \"abc\" }", buf.items);
-    buf.clearRetainingCapacity();
+    try s.value(.{ .str = "abc" }, .{});
+    try std.testing.expectEqualStrings(".{ .str = \"abc\" }", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.value(.{ .str = "abc" }, .{ .emit_strings_as_containers = true });
+    try s.value(.{ .str = "abc" }, .{ .emit_strings_as_containers = true });
     try std.testing.expectEqualStrings(
         \\.{ .str = .{
         \\    97,
         \\    98,
         \\    99,
         \\} }
-    , buf.items);
-    buf.clearRetainingCapacity();
+    , aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Arrays (rather than pointers to arrays) of u8s are not considered strings, so that data can
     // round trip correctly.
-    try sz.value("abc".*, .{});
+    try s.value("abc".*, .{});
     try std.testing.expectEqualStrings(
         \\.{
         \\    97,
         \\    98,
         \\    99,
         \\}
-    , buf.items);
-    buf.clearRetainingCapacity();
+    , aw.getWritten());
+    aw.clearRetainingCapacity();
 }
 
 test "std.zon stringify multiline strings" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     inline for (.{ true, false }) |whitespace| {
-        sz.options.whitespace = whitespace;
+        s.options.whitespace = whitespace;
 
         {
-            try sz.multilineString("", .{ .top_level = true });
-            try std.testing.expectEqualStrings("\\\\", buf.items);
-            buf.clearRetainingCapacity();
+            try s.multilineString("", .{ .top_level = true });
+            try std.testing.expectEqualStrings("\\\\", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            try sz.multilineString("abc⚡", .{ .top_level = true });
-            try std.testing.expectEqualStrings("\\\\abc⚡", buf.items);
-            buf.clearRetainingCapacity();
+            try s.multilineString("abc⚡", .{ .top_level = true });
+            try std.testing.expectEqualStrings("\\\\abc⚡", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            try sz.multilineString("abc⚡\ndef", .{ .top_level = true });
-            try std.testing.expectEqualStrings("\\\\abc⚡\n\\\\def", buf.items);
-            buf.clearRetainingCapacity();
+            try s.multilineString("abc⚡\ndef", .{ .top_level = true });
+            try std.testing.expectEqualStrings("\\\\abc⚡\n\\\\def", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            try sz.multilineString("abc⚡\r\ndef", .{ .top_level = true });
-            try std.testing.expectEqualStrings("\\\\abc⚡\n\\\\def", buf.items);
-            buf.clearRetainingCapacity();
+            try s.multilineString("abc⚡\r\ndef", .{ .top_level = true });
+            try std.testing.expectEqualStrings("\\\\abc⚡\n\\\\def", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            try sz.multilineString("\nabc⚡", .{ .top_level = true });
-            try std.testing.expectEqualStrings("\\\\\n\\\\abc⚡", buf.items);
-            buf.clearRetainingCapacity();
+            try s.multilineString("\nabc⚡", .{ .top_level = true });
+            try std.testing.expectEqualStrings("\\\\\n\\\\abc⚡", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            try sz.multilineString("\r\nabc⚡", .{ .top_level = true });
-            try std.testing.expectEqualStrings("\\\\\n\\\\abc⚡", buf.items);
-            buf.clearRetainingCapacity();
+            try s.multilineString("\r\nabc⚡", .{ .top_level = true });
+            try std.testing.expectEqualStrings("\\\\\n\\\\abc⚡", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
-            try sz.multilineString("abc\ndef", .{});
+            try s.multilineString("abc\ndef", .{});
             if (whitespace) {
-                try std.testing.expectEqualStrings("\n\\\\abc\n\\\\def\n", buf.items);
+                try std.testing.expectEqualStrings("\n\\\\abc\n\\\\def\n", aw.getWritten());
             } else {
-                try std.testing.expectEqualStrings("\\\\abc\n\\\\def\n", buf.items);
+                try std.testing.expectEqualStrings("\\\\abc\n\\\\def\n", aw.getWritten());
             }
-            buf.clearRetainingCapacity();
+            aw.clearRetainingCapacity();
         }
 
         {
             const str: []const u8 = &.{ 'a', '\r', 'c' };
-            try sz.string(str);
-            try std.testing.expectEqualStrings("\"a\\rc\"", buf.items);
-            buf.clearRetainingCapacity();
+            try s.string(str);
+            try std.testing.expectEqualStrings("\"a\\rc\"", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
 
         {
             try std.testing.expectError(
                 error.InnerCarriageReturn,
-                sz.multilineString(@as([]const u8, &.{ 'a', '\r', 'c' }), .{}),
+                s.multilineString(@as([]const u8, &.{ 'a', '\r', 'c' }), .{}),
             );
             try std.testing.expectError(
                 error.InnerCarriageReturn,
-                sz.multilineString(@as([]const u8, &.{ 'a', '\r', 'c', '\n' }), .{}),
+                s.multilineString(@as([]const u8, &.{ 'a', '\r', 'c', '\n' }), .{}),
             );
             try std.testing.expectError(
                 error.InnerCarriageReturn,
-                sz.multilineString(@as([]const u8, &.{ 'a', '\r', 'c', '\r', '\n' }), .{}),
+                s.multilineString(@as([]const u8, &.{ 'a', '\r', 'c', '\r', '\n' }), .{}),
             );
-            try std.testing.expectEqualStrings("", buf.items);
-            buf.clearRetainingCapacity();
+            try std.testing.expectEqualStrings("", aw.getWritten());
+            aw.clearRetainingCapacity();
         }
     }
 }
@@ -1932,42 +1912,43 @@ test "std.zon stringify skip default fields" {
 }
 
 test "std.zon depth limits" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    const bw = &aw.writer;
+    defer aw.deinit();
 
     const Recurse = struct { r: []const @This() };
 
     // Normal operation
-    try serializeMaxDepth(.{ 1, .{ 2, 3 } }, .{}, buf.writer(), 16);
-    try std.testing.expectEqualStrings(".{ 1, .{ 2, 3 } }", buf.items);
-    buf.clearRetainingCapacity();
+    try serializeMaxDepth(.{ 1, .{ 2, 3 } }, .{}, bw, 16);
+    try std.testing.expectEqualStrings(".{ 1, .{ 2, 3 } }", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try serializeArbitraryDepth(.{ 1, .{ 2, 3 } }, .{}, buf.writer());
-    try std.testing.expectEqualStrings(".{ 1, .{ 2, 3 } }", buf.items);
-    buf.clearRetainingCapacity();
+    try serializeArbitraryDepth(.{ 1, .{ 2, 3 } }, .{}, bw);
+    try std.testing.expectEqualStrings(".{ 1, .{ 2, 3 } }", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Max depth failing on non recursive type
     try std.testing.expectError(
         error.ExceededMaxDepth,
-        serializeMaxDepth(.{ 1, .{ 2, .{ 3, 4 } } }, .{}, buf.writer(), 3),
+        serializeMaxDepth(.{ 1, .{ 2, .{ 3, 4 } } }, .{}, bw, 3),
     );
-    try std.testing.expectEqualStrings("", buf.items);
-    buf.clearRetainingCapacity();
+    try std.testing.expectEqualStrings("", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Max depth passing on recursive type
     {
         const maybe_recurse = Recurse{ .r = &.{} };
-        try serializeMaxDepth(maybe_recurse, .{}, buf.writer(), 2);
-        try std.testing.expectEqualStrings(".{ .r = .{} }", buf.items);
-        buf.clearRetainingCapacity();
+        try serializeMaxDepth(maybe_recurse, .{}, bw, 2);
+        try std.testing.expectEqualStrings(".{ .r = .{} }", aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // Unchecked passing on recursive type
     {
         const maybe_recurse = Recurse{ .r = &.{} };
-        try serializeArbitraryDepth(maybe_recurse, .{}, buf.writer());
-        try std.testing.expectEqualStrings(".{ .r = .{} }", buf.items);
-        buf.clearRetainingCapacity();
+        try serializeArbitraryDepth(maybe_recurse, .{}, bw);
+        try std.testing.expectEqualStrings(".{ .r = .{} }", aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // Max depth failing on recursive type due to depth
@@ -1976,10 +1957,10 @@ test "std.zon depth limits" {
         maybe_recurse.r = &.{.{ .r = &.{} }};
         try std.testing.expectError(
             error.ExceededMaxDepth,
-            serializeMaxDepth(maybe_recurse, .{}, buf.writer(), 2),
+            serializeMaxDepth(maybe_recurse, .{}, bw, 2),
         );
-        try std.testing.expectEqualStrings("", buf.items);
-        buf.clearRetainingCapacity();
+        try std.testing.expectEqualStrings("", aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // Same but for a slice
@@ -1989,23 +1970,23 @@ test "std.zon depth limits" {
 
         try std.testing.expectError(
             error.ExceededMaxDepth,
-            serializeMaxDepth(maybe_recurse, .{}, buf.writer(), 2),
+            serializeMaxDepth(maybe_recurse, .{}, bw, 2),
         );
-        try std.testing.expectEqualStrings("", buf.items);
-        buf.clearRetainingCapacity();
+        try std.testing.expectEqualStrings("", aw.getWritten());
+        aw.clearRetainingCapacity();
 
-        var sz = serializer(buf.writer(), .{});
+        var s: Serializer = .{ .writer = bw };
 
         try std.testing.expectError(
             error.ExceededMaxDepth,
-            sz.tupleMaxDepth(maybe_recurse, .{}, 2),
+            s.tupleMaxDepth(maybe_recurse, .{}, 2),
         );
-        try std.testing.expectEqualStrings("", buf.items);
-        buf.clearRetainingCapacity();
+        try std.testing.expectEqualStrings("", aw.getWritten());
+        aw.clearRetainingCapacity();
 
-        try sz.tupleArbitraryDepth(maybe_recurse, .{});
-        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", buf.items);
-        buf.clearRetainingCapacity();
+        try s.tupleArbitraryDepth(maybe_recurse, .{});
+        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // A slice succeeding
@@ -2013,19 +1994,19 @@ test "std.zon depth limits" {
         var temp: [1]Recurse = .{.{ .r = &.{} }};
         const maybe_recurse: []const Recurse = &temp;
 
-        try serializeMaxDepth(maybe_recurse, .{}, buf.writer(), 3);
-        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", buf.items);
-        buf.clearRetainingCapacity();
+        try serializeMaxDepth(maybe_recurse, .{}, bw, 3);
+        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", aw.getWritten());
+        aw.clearRetainingCapacity();
 
-        var sz = serializer(buf.writer(), .{});
+        var s: Serializer = .{ .writer = bw };
 
-        try sz.tupleMaxDepth(maybe_recurse, .{}, 3);
-        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", buf.items);
-        buf.clearRetainingCapacity();
+        try s.tupleMaxDepth(maybe_recurse, .{}, 3);
+        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", aw.getWritten());
+        aw.clearRetainingCapacity();
 
-        try sz.tupleArbitraryDepth(maybe_recurse, .{});
-        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", buf.items);
-        buf.clearRetainingCapacity();
+        try s.tupleArbitraryDepth(maybe_recurse, .{});
+        try std.testing.expectEqualStrings(".{.{ .r = .{} }}", aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // Max depth failing on recursive type due to recursion
@@ -2036,46 +2017,46 @@ test "std.zon depth limits" {
 
         try std.testing.expectError(
             error.ExceededMaxDepth,
-            serializeMaxDepth(maybe_recurse, .{}, buf.writer(), 128),
+            serializeMaxDepth(maybe_recurse, .{}, bw, 128),
         );
-        try std.testing.expectEqualStrings("", buf.items);
-        buf.clearRetainingCapacity();
+        try std.testing.expectEqualStrings("", aw.getWritten());
+        aw.clearRetainingCapacity();
 
-        var sz = serializer(buf.writer(), .{});
+        var s: Serializer = .{ .writer = bw };
         try std.testing.expectError(
             error.ExceededMaxDepth,
-            sz.tupleMaxDepth(maybe_recurse, .{}, 128),
+            s.tupleMaxDepth(maybe_recurse, .{}, 128),
         );
-        try std.testing.expectEqualStrings("", buf.items);
-        buf.clearRetainingCapacity();
+        try std.testing.expectEqualStrings("", aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // Max depth on other parts of the lower level API
     {
-        var sz = serializer(buf.writer(), .{});
+        var s: Serializer = .{ .writer = bw };
 
         const maybe_recurse: []const Recurse = &.{};
 
-        try std.testing.expectError(error.ExceededMaxDepth, sz.valueMaxDepth(1, .{}, 0));
-        try sz.valueMaxDepth(2, .{}, 1);
-        try sz.value(3, .{});
-        try sz.valueArbitraryDepth(maybe_recurse, .{});
+        try std.testing.expectError(error.ExceededMaxDepth, s.valueMaxDepth(1, .{}, 0));
+        try s.valueMaxDepth(2, .{}, 1);
+        try s.value(3, .{});
+        try s.valueArbitraryDepth(maybe_recurse, .{});
 
-        var s = try sz.beginStruct(.{});
-        try std.testing.expectError(error.ExceededMaxDepth, s.fieldMaxDepth("a", 1, .{}, 0));
-        try s.fieldMaxDepth("b", 4, .{}, 1);
-        try s.field("c", 5, .{});
-        try s.fieldArbitraryDepth("d", maybe_recurse, .{});
-        try s.end();
+        var wip_struct = try s.beginStruct(.{});
+        try std.testing.expectError(error.ExceededMaxDepth, wip_struct.fieldMaxDepth("a", 1, .{}, 0));
+        try wip_struct.fieldMaxDepth("b", 4, .{}, 1);
+        try wip_struct.field("c", 5, .{});
+        try wip_struct.fieldArbitraryDepth("d", maybe_recurse, .{});
+        try wip_struct.end();
 
-        var t = try sz.beginTuple(.{});
+        var t = try s.beginTuple(.{});
         try std.testing.expectError(error.ExceededMaxDepth, t.fieldMaxDepth(1, .{}, 0));
         try t.fieldMaxDepth(6, .{}, 1);
         try t.field(7, .{});
         try t.fieldArbitraryDepth(maybe_recurse, .{});
         try t.end();
 
-        var a = try sz.beginTuple(.{});
+        var a = try s.beginTuple(.{});
         try std.testing.expectError(error.ExceededMaxDepth, a.fieldMaxDepth(1, .{}, 0));
         try a.fieldMaxDepth(8, .{}, 1);
         try a.field(9, .{});
@@ -2096,7 +2077,7 @@ test "std.zon depth limits" {
             \\    9,
             \\    .{},
             \\}
-        , buf.items);
+        , aw.getWritten());
     }
 }
 
@@ -2192,42 +2173,42 @@ test "std.zon stringify primitives" {
 }
 
 test "std.zon stringify ident" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     try expectSerializeEqual(".{ .a = 0 }", .{ .a = 0 }, .{});
-    try sz.ident("a");
-    try std.testing.expectEqualStrings(".a", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("a");
+    try std.testing.expectEqualStrings(".a", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("foo_1");
-    try std.testing.expectEqualStrings(".foo_1", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("foo_1");
+    try std.testing.expectEqualStrings(".foo_1", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("_foo_1");
-    try std.testing.expectEqualStrings("._foo_1", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("_foo_1");
+    try std.testing.expectEqualStrings("._foo_1", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("foo bar");
-    try std.testing.expectEqualStrings(".@\"foo bar\"", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("foo bar");
+    try std.testing.expectEqualStrings(".@\"foo bar\"", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("1foo");
-    try std.testing.expectEqualStrings(".@\"1foo\"", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("1foo");
+    try std.testing.expectEqualStrings(".@\"1foo\"", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("var");
-    try std.testing.expectEqualStrings(".@\"var\"", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("var");
+    try std.testing.expectEqualStrings(".@\"var\"", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("true");
-    try std.testing.expectEqualStrings(".true", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("true");
+    try std.testing.expectEqualStrings(".true", aw.getWritten());
+    aw.clearRetainingCapacity();
 
-    try sz.ident("_");
-    try std.testing.expectEqualStrings("._", buf.items);
-    buf.clearRetainingCapacity();
+    try s.ident("_");
+    try std.testing.expectEqualStrings("._", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     const Enum = enum {
         @"foo bar",
@@ -2239,40 +2220,40 @@ test "std.zon stringify ident" {
 }
 
 test "std.zon stringify as tuple" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     // Tuples
-    try sz.tuple(.{ 1, 2 }, .{});
-    try std.testing.expectEqualStrings(".{ 1, 2 }", buf.items);
-    buf.clearRetainingCapacity();
+    try s.tuple(.{ 1, 2 }, .{});
+    try std.testing.expectEqualStrings(".{ 1, 2 }", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Slice
-    try sz.tuple(@as([]const u8, &.{ 1, 2 }), .{});
-    try std.testing.expectEqualStrings(".{ 1, 2 }", buf.items);
-    buf.clearRetainingCapacity();
+    try s.tuple(@as([]const u8, &.{ 1, 2 }), .{});
+    try std.testing.expectEqualStrings(".{ 1, 2 }", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Array
-    try sz.tuple([2]u8{ 1, 2 }, .{});
-    try std.testing.expectEqualStrings(".{ 1, 2 }", buf.items);
-    buf.clearRetainingCapacity();
+    try s.tuple([2]u8{ 1, 2 }, .{});
+    try std.testing.expectEqualStrings(".{ 1, 2 }", aw.getWritten());
+    aw.clearRetainingCapacity();
 }
 
 test "std.zon stringify as float" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     // Comptime float
-    try sz.float(2.5);
-    try std.testing.expectEqualStrings("2.5", buf.items);
-    buf.clearRetainingCapacity();
+    try s.float(2.5);
+    try std.testing.expectEqualStrings("2.5", aw.getWritten());
+    aw.clearRetainingCapacity();
 
     // Sized float
-    try sz.float(@as(f32, 2.5));
-    try std.testing.expectEqualStrings("2.5", buf.items);
-    buf.clearRetainingCapacity();
+    try s.float(@as(f32, 2.5));
+    try std.testing.expectEqualStrings("2.5", aw.getWritten());
+    aw.clearRetainingCapacity();
 }
 
 test "std.zon stringify vector" {
@@ -2364,13 +2345,13 @@ test "std.zon pointers" {
 }
 
 test "std.zon tuple/struct field" {
-    var buf = std.ArrayList(u8).init(std.testing.allocator);
-    defer buf.deinit();
-    var sz = serializer(buf.writer(), .{});
+    var aw: Writer.Allocating = .init(std.testing.allocator);
+    var s: Serializer = .{ .writer = &aw.writer };
+    defer aw.deinit();
 
     // Test on structs
     {
-        var root = try sz.beginStruct(.{});
+        var root = try s.beginStruct(.{});
         {
             var tuple = try root.beginTupleField("foo", .{});
             try tuple.field(0, .{});
@@ -2396,13 +2377,13 @@ test "std.zon tuple/struct field" {
             \\        .b = 1,
             \\    },
             \\}
-        , buf.items);
-        buf.clearRetainingCapacity();
+        , aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 
     // Test on tuples
     {
-        var root = try sz.beginTuple(.{});
+        var root = try s.beginTuple(.{});
         {
             var tuple = try root.beginTupleField(.{});
             try tuple.field(0, .{});
@@ -2428,7 +2409,7 @@ test "std.zon tuple/struct field" {
             \\        .b = 1,
             \\    },
             \\}
-        , buf.items);
-        buf.clearRetainingCapacity();
+        , aw.getWritten());
+        aw.clearRetainingCapacity();
     }
 }
lib/std/zig.zig
@@ -446,8 +446,8 @@ pub fn fmtString(bytes: []const u8) std.fmt.Formatter([]const u8, stringEscape)
 }
 
 /// Return a formatter for escaping a single quoted Zig string.
-pub fn fmtChar(bytes: []const u8) std.fmt.Formatter([]const u8, charEscape) {
-    return .{ .data = bytes };
+pub fn fmtChar(c: u21) std.fmt.Formatter(u21, charEscape) {
+    return .{ .data = c };
 }
 
 test fmtString {
@@ -458,9 +458,7 @@ test fmtString {
 }
 
 test fmtChar {
-    try std.testing.expectFmt(
-        \\" \\ hi \x07 \x11 " derp \'"
-    , "\"{f}\"", .{fmtChar(" \\ hi \x07 \x11 \" derp '")});
+    try std.testing.expectFmt("c \\u{26a1}", "{f} {f}", .{ fmtChar('c'), fmtChar('⚡') });
 }
 
 /// Print the string as escaped contents of a double quoted string.
@@ -480,21 +478,26 @@ pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void {
     };
 }
 
-/// Print the string as escaped contents of a single-quoted string.
-pub fn charEscape(bytes: []const u8, w: *Writer) Writer.Error!void {
-    for (bytes) |byte| switch (byte) {
+/// Print as escaped contents of a single-quoted string.
+pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void {
+    switch (codepoint) {
         '\n' => try w.writeAll("\\n"),
         '\r' => try w.writeAll("\\r"),
         '\t' => try w.writeAll("\\t"),
         '\\' => try w.writeAll("\\\\"),
-        '"' => try w.writeByte('"'),
         '\'' => try w.writeAll("\\'"),
-        ' ', '!', '#'...'&', '('...'[', ']'...'~' => try w.writeByte(byte),
+        '"', ' ', '!', '#'...'&', '('...'[', ']'...'~' => try w.writeByte(@intCast(codepoint)),
         else => {
-            try w.writeAll("\\x");
-            try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });
+            if (std.math.cast(u8, codepoint)) |byte| {
+                try w.writeAll("\\x");
+                try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });
+            } else {
+                try w.writeAll("\\u{");
+                try w.printInt(codepoint, 16, .lower, .{});
+                try w.writeByte('}');
+            }
         },
-    };
+    }
 }
 
 pub fn isValidId(bytes: []const u8) bool {