Commit 8924f81d8c

Josh Wolfe <thejoshwolfe@gmail.com>
2023-07-22 01:56:46
std.json: Unify stringify and writeStream (#16405)
1 parent a2d81c5
lib/std/json/dynamic.zig
@@ -59,44 +59,23 @@ pub const Value = union(enum) {
         stringify(self, .{}, stderr) catch return;
     }
 
-    pub fn jsonStringify(
-        value: @This(),
-        options: StringifyOptions,
-        out_stream: anytype,
-    ) @TypeOf(out_stream).Error!void {
+    pub fn jsonStringify(value: @This(), jws: anytype) !void {
         switch (value) {
-            .null => try stringify(null, options, out_stream),
-            .bool => |inner| try stringify(inner, options, out_stream),
-            .integer => |inner| try stringify(inner, options, out_stream),
-            .float => |inner| try stringify(inner, options, out_stream),
-            .number_string => |inner| try out_stream.writeAll(inner),
-            .string => |inner| try stringify(inner, options, out_stream),
-            .array => |inner| try stringify(inner.items, options, out_stream),
+            .null => try jws.write(null),
+            .bool => |inner| try jws.write(inner),
+            .integer => |inner| try jws.write(inner),
+            .float => |inner| try jws.write(inner),
+            .number_string => |inner| try jws.writePreformatted(inner),
+            .string => |inner| try jws.write(inner),
+            .array => |inner| try jws.write(inner.items),
             .object => |inner| {
-                try out_stream.writeByte('{');
-                var field_output = false;
-                var child_options = options;
-                child_options.whitespace.indent_level += 1;
+                try jws.beginObject();
                 var it = inner.iterator();
                 while (it.next()) |entry| {
-                    if (!field_output) {
-                        field_output = true;
-                    } else {
-                        try out_stream.writeByte(',');
-                    }
-                    try child_options.whitespace.outputIndent(out_stream);
-
-                    try stringify(entry.key_ptr.*, options, out_stream);
-                    try out_stream.writeByte(':');
-                    if (child_options.whitespace.separator) {
-                        try out_stream.writeByte(' ');
-                    }
-                    try stringify(entry.value_ptr.*, child_options, out_stream);
-                }
-                if (field_output) {
-                    try options.whitespace.outputIndent(out_stream);
+                    try jws.objectField(entry.key_ptr.*);
+                    try jws.write(entry.value_ptr.*);
                 }
-                try out_stream.writeByte('}');
+                try jws.endObject();
             },
         }
     }
lib/std/json/dynamic_test.zig
@@ -69,38 +69,34 @@ test "json.parser.dynamic" {
     try testing.expect(mem.eql(u8, large_int.number_string, "18446744073709551615"));
 }
 
-const writeStream = @import("./write_stream.zig").writeStream;
+const writeStream = @import("./stringify.zig").writeStream;
 test "write json then parse it" {
     var out_buffer: [1000]u8 = undefined;
 
     var fixed_buffer_stream = std.io.fixedBufferStream(&out_buffer);
     const out_stream = fixed_buffer_stream.writer();
-    var jw = writeStream(out_stream, 4);
+    var jw = writeStream(out_stream, .{});
+    defer jw.deinit();
 
     try jw.beginObject();
 
     try jw.objectField("f");
-    try jw.emitBool(false);
+    try jw.write(false);
 
     try jw.objectField("t");
-    try jw.emitBool(true);
+    try jw.write(true);
 
     try jw.objectField("int");
-    try jw.emitNumber(1234);
+    try jw.write(1234);
 
     try jw.objectField("array");
     try jw.beginArray();
-
-    try jw.arrayElem();
-    try jw.emitNull();
-
-    try jw.arrayElem();
-    try jw.emitNumber(12.34);
-
+    try jw.write(null);
+    try jw.write(12.34);
     try jw.endArray();
 
     try jw.objectField("str");
-    try jw.emitString("hello");
+    try jw.write("hello");
 
     try jw.endObject();
 
@@ -185,64 +181,50 @@ test "escaped characters" {
 }
 
 test "Value.jsonStringify" {
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        try @as(Value, .null).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "null");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        try (Value{ .bool = true }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "true");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        try (Value{ .integer = 42 }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "42");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        try (Value{ .number_string = "43" }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "43");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        try (Value{ .float = 42 }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "4.2e+01");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        try (Value{ .string = "weeee" }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "\"weeee\"");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        var vals = [_]Value{
-            .{ .integer = 1 },
-            .{ .integer = 2 },
-            .{ .number_string = "3" },
-        };
-        try (Value{
-            .array = Array.fromOwnedSlice(undefined, &vals),
-        }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "[1,2,3]");
-    }
-    {
-        var buffer: [10]u8 = undefined;
-        var fbs = std.io.fixedBufferStream(&buffer);
-        var obj = ObjectMap.init(testing.allocator);
-        defer obj.deinit();
-        try obj.putNoClobber("a", .{ .string = "b" });
-        try (Value{ .object = obj }).jsonStringify(.{}, fbs.writer());
-        try testing.expectEqualSlices(u8, fbs.getWritten(), "{\"a\":\"b\"}");
-    }
+    var vals = [_]Value{
+        .{ .integer = 1 },
+        .{ .integer = 2 },
+        .{ .number_string = "3" },
+    };
+    var obj = ObjectMap.init(testing.allocator);
+    defer obj.deinit();
+    try obj.putNoClobber("a", .{ .string = "b" });
+    var array = [_]Value{
+        Value.null,
+        Value{ .bool = true },
+        Value{ .integer = 42 },
+        Value{ .number_string = "43" },
+        Value{ .float = 42 },
+        Value{ .string = "weeee" },
+        Value{ .array = Array.fromOwnedSlice(undefined, &vals) },
+        Value{ .object = obj },
+    };
+    var buffer: [0x1000]u8 = undefined;
+    var fbs = std.io.fixedBufferStream(&buffer);
+
+    var jw = writeStream(fbs.writer(), .{ .whitespace = .indent_1 });
+    defer jw.deinit();
+    try jw.write(array);
+
+    const expected =
+        \\[
+        \\ null,
+        \\ true,
+        \\ 42,
+        \\ 43,
+        \\ 4.2e+01,
+        \\ "weeee",
+        \\ [
+        \\  1,
+        \\  2,
+        \\  3
+        \\ ],
+        \\ {
+        \\  "a": "b"
+        \\ }
+        \\]
+    ;
+    try testing.expectEqualSlices(u8, expected, fbs.getWritten());
 }
 
 test "parseFromValue(std.json.Value,...)" {
lib/std/json/hashmap.zig
@@ -5,9 +5,6 @@ const ParseOptions = @import("static.zig").ParseOptions;
 const innerParse = @import("static.zig").innerParse;
 const innerParseFromValue = @import("static.zig").innerParseFromValue;
 const Value = @import("dynamic.zig").Value;
-const StringifyOptions = @import("stringify.zig").StringifyOptions;
-const stringify = @import("stringify.zig").stringify;
-const encodeJsonString = @import("stringify.zig").encodeJsonString;
 
 /// A thin wrapper around `std.StringArrayHashMapUnmanaged` that implements
 /// `jsonParse`, `jsonParseFromValue`, and `jsonStringify`.
@@ -70,30 +67,14 @@ pub fn ArrayHashMap(comptime T: type) type {
             return .{ .map = map };
         }
 
-        pub fn jsonStringify(self: @This(), options: StringifyOptions, out_stream: anytype) !void {
-            try out_stream.writeByte('{');
-            var field_output = false;
-            var child_options = options;
-            child_options.whitespace.indent_level += 1;
+        pub fn jsonStringify(self: @This(), jws: anytype) !void {
+            try jws.beginObject();
             var it = self.map.iterator();
             while (it.next()) |kv| {
-                if (!field_output) {
-                    field_output = true;
-                } else {
-                    try out_stream.writeByte(',');
-                }
-                try child_options.whitespace.outputIndent(out_stream);
-                try encodeJsonString(kv.key_ptr.*, options, out_stream);
-                try out_stream.writeByte(':');
-                if (child_options.whitespace.separator) {
-                    try out_stream.writeByte(' ');
-                }
-                try stringify(kv.value_ptr.*, child_options, out_stream);
-            }
-            if (field_output) {
-                try options.whitespace.outputIndent(out_stream);
+                try jws.objectField(kv.key_ptr.*);
+                try jws.write(kv.value_ptr.*);
             }
-            try out_stream.writeByte('}');
+            try jws.endObject();
         }
     };
 }
lib/std/json/hashmap_test.zig
@@ -101,11 +101,7 @@ test "stringify json hashmap whitespace" {
     try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" });
 
     {
-        const doc = try stringifyAlloc(testing.allocator, value, .{
-            .whitespace = .{
-                .indent = .{ .space = 2 },
-            },
-        });
+        const doc = try stringifyAlloc(testing.allocator, value, .{ .whitespace = .indent_2 });
         defer testing.allocator.free(doc);
         try testing.expectEqualStrings(
             \\{
lib/std/json/scanner.zig
@@ -33,6 +33,7 @@ const std = @import("std");
 const Allocator = std.mem.Allocator;
 const ArrayList = std.ArrayList;
 const assert = std.debug.assert;
+const BitStack = std.BitStack;
 
 /// Scan the input and check for malformed JSON.
 /// On `SyntaxError` or `UnexpectedEndOfInput`, returns `false`.
@@ -337,7 +338,7 @@ pub fn Reader(comptime buffer_size: usize, comptime ReaderType: type) type {
             }
         }
         /// Like `std.json.Scanner.skipUntilStackHeight()` but handles `error.BufferUnderrun`.
-        pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: u32) NextError!void {
+        pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: usize) NextError!void {
             while (true) {
                 return self.scanner.skipUntilStackHeight(terminal_stack_height) catch |err| switch (err) {
                     error.BufferUnderrun => {
@@ -350,11 +351,11 @@ pub fn Reader(comptime buffer_size: usize, comptime ReaderType: type) type {
         }
 
         /// Calls `std.json.Scanner.stackHeight`.
-        pub fn stackHeight(self: *const @This()) u32 {
+        pub fn stackHeight(self: *const @This()) usize {
             return self.scanner.stackHeight();
         }
         /// Calls `std.json.Scanner.ensureTotalStackCapacity`.
-        pub fn ensureTotalStackCapacity(self: *@This(), height: u32) Allocator.Error!void {
+        pub fn ensureTotalStackCapacity(self: *@This(), height: usize) Allocator.Error!void {
             try self.scanner.ensureTotalStackCapacity(height);
         }
 
@@ -654,7 +655,7 @@ pub const Scanner = struct {
 
     /// Skip tokens until an `.object_end` or `.array_end` token results in a `stackHeight()` equal the given stack height.
     /// Unlike `skipValue()`, this function is available in streaming mode.
-    pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: u32) NextError!void {
+    pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: usize) NextError!void {
         while (true) {
             switch (try self.next()) {
                 .object_end, .array_end => {
@@ -667,13 +668,13 @@ pub const Scanner = struct {
     }
 
     /// The depth of `{}` or `[]` nesting levels at the current position.
-    pub fn stackHeight(self: *const @This()) u32 {
+    pub fn stackHeight(self: *const @This()) usize {
         return self.stack.bit_len;
     }
 
     /// Pre allocate memory to hold the given number of nesting levels.
     /// `stackHeight()` up to the given number will not cause allocations.
-    pub fn ensureTotalStackCapacity(self: *@This(), height: u32) Allocator.Error!void {
+    pub fn ensureTotalStackCapacity(self: *@This(), height: usize) Allocator.Error!void {
         try self.stack.ensureTotalCapacity(height);
     }
 
@@ -1697,53 +1698,6 @@ pub const Scanner = struct {
 const OBJECT_MODE = 0;
 const ARRAY_MODE = 1;
 
-const BitStack = struct {
-    bytes: std.ArrayList(u8),
-    bit_len: u32 = 0,
-
-    pub fn init(allocator: Allocator) @This() {
-        return .{
-            .bytes = std.ArrayList(u8).init(allocator),
-        };
-    }
-
-    pub fn deinit(self: *@This()) void {
-        self.bytes.deinit();
-        self.* = undefined;
-    }
-
-    pub fn ensureTotalCapacity(self: *@This(), bit_capcity: u32) Allocator.Error!void {
-        const byte_capacity = (bit_capcity + 7) >> 3;
-        try self.bytes.ensureTotalCapacity(byte_capacity);
-    }
-
-    pub fn push(self: *@This(), b: u1) Allocator.Error!void {
-        const byte_index = self.bit_len >> 3;
-        const bit_index = @as(u3, @intCast(self.bit_len & 7));
-
-        if (self.bytes.items.len <= byte_index) {
-            try self.bytes.append(0);
-        }
-
-        self.bytes.items[byte_index] &= ~(@as(u8, 1) << bit_index);
-        self.bytes.items[byte_index] |= @as(u8, b) << bit_index;
-
-        self.bit_len += 1;
-    }
-
-    pub fn peek(self: *const @This()) u1 {
-        const byte_index = (self.bit_len - 1) >> 3;
-        const bit_index = @as(u3, @intCast((self.bit_len - 1) & 7));
-        return @as(u1, @intCast((self.bytes.items[byte_index] >> bit_index) & 1));
-    }
-
-    pub fn pop(self: *@This()) u1 {
-        const b = self.peek();
-        self.bit_len -= 1;
-        return b;
-    }
-};
-
 fn appendSlice(list: *std.ArrayList(u8), buf: []const u8, max_value_len: usize) !void {
     const new_len = std.math.add(usize, list.items.len, buf.len) catch return error.ValueTooLong;
     if (new_len > max_value_len) return error.ValueTooLong;
lib/std/json/stringify.zig
@@ -1,73 +1,583 @@
 const std = @import("std");
-const mem = std.mem;
 const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const ArrayList = std.ArrayList;
+const BitStack = std.BitStack;
+
+const OBJECT_MODE = 0;
+const ARRAY_MODE = 1;
 
 pub const StringifyOptions = struct {
-    pub const Whitespace = struct {
-        /// How many indentation levels deep are we?
+    /// Controls the whitespace emitted.
+    /// The default `.minified` is a compact encoding with no whitespace between tokens.
+    /// Any setting other than `.minified` will use newlines, indentation, and a space after each ':'.
+    /// `.indent_1` means 1 space for each indentation level, `.indent_2` means 2 spaces, etc.
+    /// `.indent_tab` uses a tab for each indentation level.
+    whitespace: enum {
+        minified,
+        indent_1,
+        indent_2,
+        indent_3,
+        indent_4,
+        indent_8,
+        indent_tab,
+    } = .minified,
+
+    /// Should optional fields with null value be written?
+    emit_null_optional_fields: bool = true,
+
+    /// Arrays/slices of u8 are typically encoded as JSON strings.
+    /// This option emits them as arrays of numbers instead.
+    /// Does not affect calls to `objectField()`.
+    emit_strings_as_arrays: bool = false,
+
+    /// Should unicode characters be escaped in strings?
+    escape_unicode: bool = false,
+};
+
+/// Writes the given value to the `std.io.Writer` stream.
+/// See `WriteStream` for how the given value is serialized into JSON.
+/// The maximum nesting depth of the output JSON document is 256.
+/// See also `stringifyMaxDepth` and `stringifyArbitraryDepth`.
+pub fn stringify(
+    value: anytype,
+    options: StringifyOptions,
+    out_stream: anytype,
+) @TypeOf(out_stream).Error!void {
+    var jw = writeStream(out_stream, options);
+    defer jw.deinit();
+    try jw.write(value);
+}
+
+/// Like `stringify` with configurable nesting depth.
+/// `max_depth` is rounded up to the nearest multiple of 8.
+/// Give `null` for `max_depth` to disable some safety checks and allow arbitrary nesting depth.
+/// See `writeStreamMaxDepth` for more info.
+pub fn stringifyMaxDepth(
+    value: anytype,
+    options: StringifyOptions,
+    out_stream: anytype,
+    comptime max_depth: ?usize,
+) @TypeOf(out_stream).Error!void {
+    var jw = writeStreamMaxDepth(out_stream, options, max_depth);
+    try jw.write(value);
+}
+
+/// Like `stringify` but takes an allocator to facilitate safety checks while allowing arbitrary nesting depth.
+/// These safety checks can be helpful when debugging custom `jsonStringify` implementations;
+/// See `WriteStream`.
+pub fn stringifyArbitraryDepth(
+    allocator: Allocator,
+    value: anytype,
+    options: StringifyOptions,
+    out_stream: anytype,
+) WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth).Error!void {
+    var jw = writeStreamArbitraryDepth(allocator, out_stream, options);
+    defer jw.deinit();
+    try jw.write(value);
+}
+
+/// Calls `stringifyArbitraryDepth` and stores the result in dynamically allocated memory
+/// instead of taking a `std.io.Writer`.
+///
+/// Caller owns returned memory.
+pub fn stringifyAlloc(
+    allocator: Allocator,
+    value: anytype,
+    options: StringifyOptions,
+) error{OutOfMemory}![]const u8 {
+    var list = std.ArrayList(u8).init(allocator);
+    errdefer list.deinit();
+    try stringifyArbitraryDepth(allocator, value, options, list.writer());
+    return list.toOwnedSlice();
+}
+
+/// See `WriteStream` for documentation.
+/// Equivalent to calling `writeStreamMaxDepth` with a depth of `256`.
+///
+/// The caller does *not* need to call `deinit()` on the returned object.
+pub fn writeStream(
+    out_stream: anytype,
+    options: StringifyOptions,
+) WriteStream(@TypeOf(out_stream), .{ .checked_to_fixed_depth = 256 }) {
+    return writeStreamMaxDepth(out_stream, options, 256);
+}
+
+/// See `WriteStream` for documentation.
+/// The returned object includes 1 bit of size per `max_depth` to enable safety checks on the order of method calls;
+/// see the grammar in the `WriteStream` documentation.
+/// `max_depth` is rounded up to the nearest multiple of 8.
+/// If the nesting depth exceeds `max_depth`, it is detectable illegal behavior.
+/// Give `null` for `max_depth` to disable safety checks for the grammar and allow arbitrary nesting depth.
+/// Alternatively, see `writeStreamArbitraryDepth` to do safety checks to arbitrary depth.
+///
+/// The caller does *not* need to call `deinit()` on the returned object.
+pub fn writeStreamMaxDepth(
+    out_stream: anytype,
+    options: StringifyOptions,
+    comptime max_depth: ?usize,
+) WriteStream(
+    @TypeOf(out_stream),
+    if (max_depth) |d| .{ .checked_to_fixed_depth = d } else .assumed_correct,
+) {
+    return WriteStream(
+        @TypeOf(out_stream),
+        if (max_depth) |d| .{ .checked_to_fixed_depth = d } else .assumed_correct,
+    ).init(undefined, out_stream, options);
+}
+
+/// See `WriteStream` for documentation.
+/// This version of the write stream enables safety checks to arbitrarily deep nesting levels
+/// by using the given allocator.
+/// The caller should call `deinit()` on the returned object to free allocated memory.
+pub fn writeStreamArbitraryDepth(
+    allocator: Allocator,
+    out_stream: anytype,
+    options: StringifyOptions,
+) WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth) {
+    return WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth).init(allocator, out_stream, options);
+}
+
+/// Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data
+/// to a stream.
+///
+/// The seqeunce of method calls to write JSON content must follow this grammar:
+/// ```
+///  <once> = <value>
+///  <value> =
+///    | <object>
+///    | <array>
+///    | write
+///    | writePreformatted
+///  <object> = beginObject ( objectField <value> )* endObject
+///  <array> = beginArray ( <value> )* endArray
+/// ```
+///
+/// Supported types:
+///  * Zig `bool` -> JSON `true` or `false`.
+///  * Zig `?T` -> `null` or the rendering of `T`.
+///  * Zig `i32`, `u64`, etc. -> JSON number or string.
+///      * If the value is outside the range `ยฑ1<<53` (the precise integer rage of f64), it is rendered as a JSON string in base 10. Otherwise, it is rendered as JSON number.
+///  * Zig floats -> JSON number or string.
+///      * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
+///      * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
+///  * Zig `[]const u8`, `[]u8`, `*[N]u8`, `@Vector(N, u8)`, and similar -> JSON string.
+///      * See `StringifyOptions.emit_strings_as_arrays`.
+///      * If the content is not valid UTF-8, rendered as an array of numbers instead.
+///  * Zig `[]T`, `[N]T`, `*[N]T`, `@Vector(N, T)`, and similar -> JSON array of the rendering of each item.
+///  * Zig tuple -> JSON array of the rendering of each item.
+///  * Zig `struct` -> JSON object with each field in declaration order.
+///      * If the struct declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `WriteStream`. See `std.json.Value` for an example.
+///      * See `StringifyOptions.emit_null_optional_fields`.
+///  * Zig `union(enum)` -> JSON object with one field named for the active tag and a value representing the payload.
+///      * If the payload is `void`, then the emitted value is `{}`.
+///      * If the union declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `WriteStream`.
+///  * Zig `enum` -> JSON string naming the active tag.
+///      * If the enum declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `WriteStream`.
+///  * Zig error -> JSON string naming the error.
+///  * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion.
+pub fn WriteStream(
+    comptime OutStream: type,
+    comptime safety_checks: union(enum) {
+        checked_to_arbitrary_depth,
+        checked_to_fixed_depth: usize, // Rounded up to the nearest multiple of 8.
+        assumed_correct,
+    },
+) type {
+    return struct {
+        const Self = @This();
+
+        pub const Stream = OutStream;
+        pub const Error = switch (safety_checks) {
+            .checked_to_arbitrary_depth => Stream.Error || error{OutOfMemory},
+            .checked_to_fixed_depth, .assumed_correct => Stream.Error,
+        };
+
+        options: StringifyOptions,
+
+        stream: OutStream,
         indent_level: usize = 0,
+        next_punctuation: enum {
+            the_beginning,
+            none,
+            comma,
+            colon,
+        } = .the_beginning,
+
+        nesting_stack: switch (safety_checks) {
+            .checked_to_arbitrary_depth => BitStack,
+            .checked_to_fixed_depth => |fixed_buffer_size| [(fixed_buffer_size + 7) >> 3]u8,
+            .assumed_correct => void,
+        },
 
-        /// What character(s) should be used for indentation?
-        indent: union(enum) {
-            space: u8,
-            tab: void,
-            none: void,
-        } = .{ .space = 4 },
-
-        /// After a colon, should whitespace be inserted?
-        separator: bool = true,
-
-        pub fn outputIndent(
-            whitespace: @This(),
-            out_stream: anytype,
-        ) @TypeOf(out_stream).Error!void {
-            var char: u8 = undefined;
-            var n_chars: usize = undefined;
-            switch (whitespace.indent) {
-                .space => |n_spaces| {
-                    char = ' ';
-                    n_chars = n_spaces;
+        pub fn init(safety_allocator: Allocator, stream: OutStream, options: StringifyOptions) Self {
+            return .{
+                .options = options,
+                .stream = stream,
+                .nesting_stack = switch (safety_checks) {
+                    .checked_to_arbitrary_depth => BitStack.init(safety_allocator),
+                    .checked_to_fixed_depth => |fixed_buffer_size| [_]u8{0} ** ((fixed_buffer_size + 7) >> 3),
+                    .assumed_correct => {},
                 },
-                .tab => {
+            };
+        }
+
+        pub fn deinit(self: *Self) void {
+            switch (safety_checks) {
+                .checked_to_arbitrary_depth => self.nesting_stack.deinit(),
+                .checked_to_fixed_depth, .assumed_correct => {},
+            }
+            self.* = undefined;
+        }
+
+        pub fn beginArray(self: *Self) Error!void {
+            try self.valueStart();
+            try self.stream.writeByte('[');
+            try self.pushIndentation(ARRAY_MODE);
+            self.next_punctuation = .none;
+        }
+
+        pub fn beginObject(self: *Self) Error!void {
+            try self.valueStart();
+            try self.stream.writeByte('{');
+            try self.pushIndentation(OBJECT_MODE);
+            self.next_punctuation = .none;
+        }
+
+        pub fn endArray(self: *Self) Error!void {
+            self.popIndentation(ARRAY_MODE);
+            switch (self.next_punctuation) {
+                .none => {},
+                .comma => {
+                    try self.indent();
+                },
+                .the_beginning, .colon => unreachable,
+            }
+            try self.stream.writeByte(']');
+            self.valueDone();
+        }
+
+        pub fn endObject(self: *Self) Error!void {
+            self.popIndentation(OBJECT_MODE);
+            switch (self.next_punctuation) {
+                .none => {},
+                .comma => {
+                    try self.indent();
+                },
+                .the_beginning, .colon => unreachable,
+            }
+            try self.stream.writeByte('}');
+            self.valueDone();
+        }
+
+        fn pushIndentation(self: *Self, mode: u1) !void {
+            switch (safety_checks) {
+                .checked_to_arbitrary_depth => {
+                    try self.nesting_stack.push(mode);
+                    self.indent_level += 1;
+                },
+                .checked_to_fixed_depth => {
+                    BitStack.pushWithStateAssumeCapacity(&self.nesting_stack, &self.indent_level, mode);
+                },
+                .assumed_correct => {
+                    self.indent_level += 1;
+                },
+            }
+        }
+        fn popIndentation(self: *Self, assert_its_this_one: u1) void {
+            switch (safety_checks) {
+                .checked_to_arbitrary_depth => {
+                    assert(self.nesting_stack.pop() == assert_its_this_one);
+                    self.indent_level -= 1;
+                },
+                .checked_to_fixed_depth => {
+                    assert(BitStack.popWithState(&self.nesting_stack, &self.indent_level) == assert_its_this_one);
+                },
+                .assumed_correct => {
+                    self.indent_level -= 1;
+                },
+            }
+        }
+
+        fn indent(self: *Self) !void {
+            var char: u8 = ' ';
+            const n_chars = switch (self.options.whitespace) {
+                .minified => return,
+                .indent_1 => 1 * self.indent_level,
+                .indent_2 => 2 * self.indent_level,
+                .indent_3 => 3 * self.indent_level,
+                .indent_4 => 4 * self.indent_level,
+                .indent_8 => 8 * self.indent_level,
+                .indent_tab => blk: {
                     char = '\t';
-                    n_chars = 1;
+                    break :blk self.indent_level;
+                },
+            };
+            try self.stream.writeByte('\n');
+            try self.stream.writeByteNTimes(char, n_chars);
+        }
+
+        fn valueStart(self: *Self) !void {
+            if (self.isObjectKeyExpected()) |is_it| assert(!is_it); // Call objectField(), not write(), for object keys.
+            return self.valueStartAssumeTypeOk();
+        }
+        fn objectFieldStart(self: *Self) !void {
+            if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField().
+            return self.valueStartAssumeTypeOk();
+        }
+        fn valueStartAssumeTypeOk(self: *Self) !void {
+            assert(!self.isComplete()); // JSON document already complete.
+            switch (self.next_punctuation) {
+                .the_beginning => {
+                    // No indentation for the very beginning.
+                },
+                .none => {
+                    // First item in a container.
+                    try self.indent();
+                },
+                .comma => {
+                    // Subsequent item in a container.
+                    try self.stream.writeByte(',');
+                    try self.indent();
+                },
+                .colon => {
+                    try self.stream.writeByte(':');
+                    if (self.options.whitespace != .minified) {
+                        try self.stream.writeByte(' ');
+                    }
                 },
-                .none => return,
             }
-            try out_stream.writeByte('\n');
-            n_chars *= whitespace.indent_level;
-            try out_stream.writeByteNTimes(char, n_chars);
         }
-    };
+        fn valueDone(self: *Self) void {
+            self.next_punctuation = .comma;
+        }
 
-    /// Controls the whitespace emitted
-    whitespace: Whitespace = .{ .indent = .none, .separator = false },
+        // Only when safety is enabled:
+        fn isObjectKeyExpected(self: *const Self) ?bool {
+            switch (safety_checks) {
+                .checked_to_arbitrary_depth => return self.indent_level > 0 and
+                    self.nesting_stack.peek() == OBJECT_MODE and
+                    self.next_punctuation != .colon,
+                .checked_to_fixed_depth => return self.indent_level > 0 and
+                    BitStack.peekWithState(&self.nesting_stack, self.indent_level) == OBJECT_MODE and
+                    self.next_punctuation != .colon,
+                .assumed_correct => return null,
+            }
+        }
+        fn isComplete(self: *const Self) bool {
+            return self.indent_level == 0 and self.next_punctuation == .comma;
+        }
 
-    /// Should optional fields with null value be written?
-    emit_null_optional_fields: bool = true,
+        /// An alternative to calling `write` that outputs the given bytes verbatim.
+        /// This function does the usual punctuation and indentation formatting
+        /// assuming the given slice represents a single complete value;
+        /// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`.
+        pub fn writePreformatted(self: *Self, value_slice: []const u8) Error!void {
+            try self.valueStart();
+            try self.stream.writeAll(value_slice);
+            self.valueDone();
+        }
 
-    string: StringOptions = StringOptions{ .String = .{} },
+        pub fn objectField(self: *Self, key: []const u8) Error!void {
+            try self.objectFieldStart();
+            try encodeJsonString(key, self.options, self.stream);
+            self.next_punctuation = .colon;
+        }
 
-    /// Should []u8 be serialised as a string? or an array?
-    pub const StringOptions = union(enum) {
-        Array,
-        String: StringOutputOptions,
+        /// See `WriteStream`.
+        pub fn write(self: *Self, value: anytype) Error!void {
+            const T = @TypeOf(value);
+            switch (@typeInfo(T)) {
+                .Int => |info| {
+                    if (info.bits < 53) {
+                        try self.valueStart();
+                        try self.stream.print("{}", .{value});
+                        self.valueDone();
+                        return;
+                    }
+                    if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) {
+                        try self.valueStart();
+                        try self.stream.print("{}", .{value});
+                        self.valueDone();
+                        return;
+                    }
+                    try self.valueStart();
+                    try self.stream.print("\"{}\"", .{value});
+                    self.valueDone();
+                    return;
+                },
+                .ComptimeInt => {
+                    return self.write(@as(std.math.IntFittingRange(value, value), value));
+                },
+                .Float, .ComptimeFloat => {
+                    if (@as(f64, @floatCast(value)) == value) {
+                        try self.valueStart();
+                        try self.stream.print("{}", .{@as(f64, @floatCast(value))});
+                        self.valueDone();
+                        return;
+                    }
+                    try self.valueStart();
+                    try self.stream.print("\"{}\"", .{value});
+                    self.valueDone();
+                    return;
+                },
 
-        /// String output options
-        const StringOutputOptions = struct {
-            /// Should '/' be escaped in strings?
-            escape_solidus: bool = false,
+                .Bool => {
+                    try self.valueStart();
+                    try self.stream.writeAll(if (value) "true" else "false");
+                    self.valueDone();
+                    return;
+                },
+                .Null => {
+                    try self.valueStart();
+                    try self.stream.writeAll("null");
+                    self.valueDone();
+                    return;
+                },
+                .Optional => {
+                    if (value) |payload| {
+                        return try self.write(payload);
+                    } else {
+                        return try self.write(null);
+                    }
+                },
+                .Enum => {
+                    if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
+                        return value.jsonStringify(self);
+                    }
 
-            /// Should unicode characters be escaped in strings?
-            escape_unicode: bool = false,
-        };
+                    return self.stringValue(@tagName(value));
+                },
+                .Union => {
+                    if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
+                        return value.jsonStringify(self);
+                    }
+
+                    const info = @typeInfo(T).Union;
+                    if (info.tag_type) |UnionTagType| {
+                        try self.beginObject();
+                        inline for (info.fields) |u_field| {
+                            if (value == @field(UnionTagType, u_field.name)) {
+                                try self.objectField(u_field.name);
+                                if (u_field.type == void) {
+                                    // void value is {}
+                                    try self.beginObject();
+                                    try self.endObject();
+                                } else {
+                                    try self.write(@field(value, u_field.name));
+                                }
+                                break;
+                            }
+                        } else {
+                            unreachable; // No active tag?
+                        }
+                        try self.endObject();
+                        return;
+                    } else {
+                        @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
+                    }
+                },
+                .Struct => |S| {
+                    if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
+                        return value.jsonStringify(self);
+                    }
+
+                    if (S.is_tuple) {
+                        try self.beginArray();
+                    } else {
+                        try self.beginObject();
+                    }
+                    inline for (S.fields) |Field| {
+                        // don't include void fields
+                        if (Field.type == void) continue;
+
+                        var emit_field = true;
+
+                        // don't include optional fields that are null when emit_null_optional_fields is set to false
+                        if (@typeInfo(Field.type) == .Optional) {
+                            if (self.options.emit_null_optional_fields == false) {
+                                if (@field(value, Field.name) == null) {
+                                    emit_field = false;
+                                }
+                            }
+                        }
+
+                        if (emit_field) {
+                            if (!S.is_tuple) {
+                                try self.objectField(Field.name);
+                            }
+                            try self.write(@field(value, Field.name));
+                        }
+                    }
+                    if (S.is_tuple) {
+                        try self.endArray();
+                    } else {
+                        try self.endObject();
+                    }
+                    return;
+                },
+                .ErrorSet => return self.stringValue(@errorName(value)),
+                .Pointer => |ptr_info| switch (ptr_info.size) {
+                    .One => switch (@typeInfo(ptr_info.child)) {
+                        .Array => {
+                            // Coerce `*[N]T` to `[]const T`.
+                            const Slice = []const std.meta.Elem(ptr_info.child);
+                            return self.write(@as(Slice, value));
+                        },
+                        else => {
+                            return self.write(value.*);
+                        },
+                    },
+                    .Many, .Slice => {
+                        if (ptr_info.size == .Many and ptr_info.sentinel == null)
+                            @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
+                        const slice = if (ptr_info.size == .Many) std.mem.span(value) else value;
+
+                        if (ptr_info.child == u8) {
+                            // This is a []const u8, or some similar Zig string.
+                            if (!self.options.emit_strings_as_arrays and std.unicode.utf8ValidateSlice(slice)) {
+                                return self.stringValue(slice);
+                            }
+                        }
+
+                        try self.beginArray();
+                        for (slice) |x| {
+                            try self.write(x);
+                        }
+                        try self.endArray();
+                        return;
+                    },
+                    else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
+                },
+                .Array => {
+                    // Coerce `[N]T` to `*const [N]T` (and then to `[]const T`).
+                    return self.write(&value);
+                },
+                .Vector => |info| {
+                    const array: [info.len]info.child = value;
+                    return self.write(&array);
+                },
+                else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
+            }
+            unreachable;
+        }
+
+        fn stringValue(self: *Self, s: []const u8) !void {
+            try self.valueStart();
+            try encodeJsonString(s, self.options, self.stream);
+            self.valueDone();
+        }
+
+        pub const arrayElem = @compileError("Deprecated; You don't need to call this anymore.");
+        pub const emitNull = @compileError("Deprecated; Use .write(null) instead.");
+        pub const emitBool = @compileError("Deprecated; Use .write() instead.");
+        pub const emitNumber = @compileError("Deprecated; Use .write() instead.");
+        pub const emitString = @compileError("Deprecated; Use .write() instead.");
+        pub const emitJson = @compileError("Deprecated; Use .write() instead.");
     };
-};
+}
 
-fn outputUnicodeEscape(
-    codepoint: u21,
-    out_stream: anytype,
-) !void {
+fn outputUnicodeEscape(codepoint: u21, out_stream: anytype) !void {
     if (codepoint <= 0xFFFF) {
         // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF),
         // then it may be represented as a six-character sequence: a reverse solidus, followed
@@ -87,6 +597,19 @@ fn outputUnicodeEscape(
     }
 }
 
+fn outputSpecialEscape(c: u8, writer: anytype) !void {
+    switch (c) {
+        '\\' => try writer.writeAll("\\\\"),
+        '\"' => try writer.writeAll("\\\""),
+        0x08 => try writer.writeAll("\\b"),
+        0x0C => try writer.writeAll("\\f"),
+        '\n' => try writer.writeAll("\\n"),
+        '\r' => try writer.writeAll("\\r"),
+        '\t' => try writer.writeAll("\\t"),
+        else => try outputUnicodeEscape(c, writer),
+    }
+}
+
 /// Write `string` to `writer` as a JSON encoded string.
 pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: anytype) !void {
     try writer.writeByte('\"');
@@ -96,218 +619,44 @@ pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: a
 
 /// Write `chars` to `writer` as JSON encoded string characters.
 pub fn encodeJsonStringChars(chars: []const u8, options: StringifyOptions, writer: anytype) !void {
+    var write_cursor: usize = 0;
     var i: usize = 0;
-    while (i < chars.len) : (i += 1) {
-        switch (chars[i]) {
-            // normal ascii character
-            0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try writer.writeByte(c),
-            // only 2 characters that *must* be escaped
-            '\\' => try writer.writeAll("\\\\"),
-            '\"' => try writer.writeAll("\\\""),
-            // solidus is optional to escape
-            '/' => {
-                if (options.string.String.escape_solidus) {
-                    try writer.writeAll("\\/");
-                } else {
-                    try writer.writeByte('/');
-                }
-            },
-            // control characters with short escapes
-            // TODO: option to switch between unicode and 'short' forms?
-            0x8 => try writer.writeAll("\\b"),
-            0xC => try writer.writeAll("\\f"),
-            '\n' => try writer.writeAll("\\n"),
-            '\r' => try writer.writeAll("\\r"),
-            '\t' => try writer.writeAll("\\t"),
-            else => {
-                const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable;
-                // control characters (only things left with 1 byte length) should always be printed as unicode escapes
-                if (ulen == 1 or options.string.String.escape_unicode) {
+    if (options.escape_unicode) {
+        while (i < chars.len) : (i += 1) {
+            switch (chars[i]) {
+                // normal ascii character
+                0x20...0x21, 0x23...0x5B, 0x5D...0x7E => {},
+                0x00...0x1F, '\\', '\"' => {
+                    // Always must escape these.
+                    try writer.writeAll(chars[write_cursor..i]);
+                    try outputSpecialEscape(chars[i], writer);
+                    write_cursor = i + 1;
+                },
+                0x7F...0xFF => {
+                    try writer.writeAll(chars[write_cursor..i]);
+                    const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable;
                     const codepoint = std.unicode.utf8Decode(chars[i..][0..ulen]) catch unreachable;
                     try outputUnicodeEscape(codepoint, writer);
-                } else {
-                    try writer.writeAll(chars[i..][0..ulen]);
-                }
-                i += ulen - 1;
-            },
-        }
-    }
-}
-
-/// If `value` has a method called `jsonStringify`, this will call that method instead of the
-/// default implementation, passing it the `options` and `out_stream` parameters.
-pub fn stringify(
-    value: anytype,
-    options: StringifyOptions,
-    out_stream: anytype,
-) @TypeOf(out_stream).Error!void {
-    const T = @TypeOf(value);
-    switch (@typeInfo(T)) {
-        .Float, .ComptimeFloat => {
-            return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, out_stream);
-        },
-        .Int, .ComptimeInt => {
-            return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, out_stream);
-        },
-        .Bool => {
-            return out_stream.writeAll(if (value) "true" else "false");
-        },
-        .Null => {
-            return out_stream.writeAll("null");
-        },
-        .Optional => {
-            if (value) |payload| {
-                return try stringify(payload, options, out_stream);
-            } else {
-                return try stringify(null, options, out_stream);
-            }
-        },
-        .Enum => {
-            if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
-                return value.jsonStringify(options, out_stream);
-            }
-
-            return try encodeJsonString(@tagName(value), options, out_stream);
-        },
-        .Union => {
-            if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
-                return value.jsonStringify(options, out_stream);
-            }
-
-            const info = @typeInfo(T).Union;
-            if (info.tag_type) |UnionTagType| {
-                try out_stream.writeByte('{');
-                var child_options = options;
-                child_options.whitespace.indent_level += 1;
-                inline for (info.fields) |u_field| {
-                    if (value == @field(UnionTagType, u_field.name)) {
-                        try child_options.whitespace.outputIndent(out_stream);
-                        try encodeJsonString(u_field.name, options, out_stream);
-                        try out_stream.writeByte(':');
-                        if (child_options.whitespace.separator) {
-                            try out_stream.writeByte(' ');
-                        }
-                        if (u_field.type == void) {
-                            try out_stream.writeAll("{}");
-                        } else {
-                            try stringify(@field(value, u_field.name), child_options, out_stream);
-                        }
-                        break;
-                    }
-                } else {
-                    unreachable; // No active tag?
-                }
-                try options.whitespace.outputIndent(out_stream);
-                try out_stream.writeByte('}');
-                return;
-            } else {
-                @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
-            }
-        },
-        .Struct => |S| {
-            if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
-                return value.jsonStringify(options, out_stream);
-            }
-
-            try out_stream.writeByte(if (S.is_tuple) '[' else '{');
-            var field_output = false;
-            var child_options = options;
-            child_options.whitespace.indent_level += 1;
-            inline for (S.fields) |Field| {
-                // don't include void fields
-                if (Field.type == void) continue;
-
-                var emit_field = true;
-
-                // don't include optional fields that are null when emit_null_optional_fields is set to false
-                if (@typeInfo(Field.type) == .Optional) {
-                    if (options.emit_null_optional_fields == false) {
-                        if (@field(value, Field.name) == null) {
-                            emit_field = false;
-                        }
-                    }
-                }
-
-                if (emit_field) {
-                    if (!field_output) {
-                        field_output = true;
-                    } else {
-                        try out_stream.writeByte(',');
-                    }
-                    try child_options.whitespace.outputIndent(out_stream);
-                    if (!S.is_tuple) {
-                        try encodeJsonString(Field.name, options, out_stream);
-                        try out_stream.writeByte(':');
-                        if (child_options.whitespace.separator) {
-                            try out_stream.writeByte(' ');
-                        }
-                    }
-                    try stringify(@field(value, Field.name), child_options, out_stream);
-                }
-            }
-            if (field_output) {
-                try options.whitespace.outputIndent(out_stream);
-            }
-            try out_stream.writeByte(if (S.is_tuple) ']' else '}');
-            return;
-        },
-        .ErrorSet => return stringify(@as([]const u8, @errorName(value)), options, out_stream),
-        .Pointer => |ptr_info| switch (ptr_info.size) {
-            .One => switch (@typeInfo(ptr_info.child)) {
-                .Array => {
-                    const Slice = []const std.meta.Elem(ptr_info.child);
-                    return stringify(@as(Slice, value), options, out_stream);
+                    i += ulen - 1;
+                    write_cursor = i + 1;
                 },
-                else => {
-                    // TODO: avoid loops?
-                    return stringify(value.*, options, out_stream);
+            }
+        }
+    } else {
+        while (i < chars.len) : (i += 1) {
+            switch (chars[i]) {
+                // normal bytes
+                0x20...0x21, 0x23...0x5B, 0x5D...0xFF => {},
+                0x00...0x1F, '\\', '\"' => {
+                    // Always must escape these.
+                    try writer.writeAll(chars[write_cursor..i]);
+                    try outputSpecialEscape(chars[i], writer);
+                    write_cursor = i + 1;
                 },
-            },
-            .Many, .Slice => {
-                if (ptr_info.size == .Many and ptr_info.sentinel == null)
-                    @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
-                const slice = if (ptr_info.size == .Many) mem.span(value) else value;
-
-                if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(slice)) {
-                    try encodeJsonString(slice, options, out_stream);
-                    return;
-                }
-
-                try out_stream.writeByte('[');
-                var child_options = options;
-                child_options.whitespace.indent_level += 1;
-                for (slice, 0..) |x, i| {
-                    if (i != 0) {
-                        try out_stream.writeByte(',');
-                    }
-                    try child_options.whitespace.outputIndent(out_stream);
-                    try stringify(x, child_options, out_stream);
-                }
-                if (slice.len != 0) {
-                    try options.whitespace.outputIndent(out_stream);
-                }
-                try out_stream.writeByte(']');
-                return;
-            },
-            else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
-        },
-        .Array => return stringify(&value, options, out_stream),
-        .Vector => |info| {
-            const array: [info.len]info.child = value;
-            return stringify(&array, options, out_stream);
-        },
-        else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
+            }
+        }
     }
-    unreachable;
-}
-
-// Same as `stringify` but accepts an Allocator and stores result in dynamically allocated memory instead of using a Writer.
-// Caller owns returned memory.
-pub fn stringifyAlloc(allocator: std.mem.Allocator, value: anytype, options: StringifyOptions) ![]const u8 {
-    var list = std.ArrayList(u8).init(allocator);
-    errdefer list.deinit();
-    try stringify(value, options, list.writer());
-    return list.toOwnedSlice();
+    try writer.writeAll(chars[write_cursor..chars.len]);
 }
 
 test {
lib/std/json/stringify_test.zig
@@ -2,9 +2,99 @@ const std = @import("std");
 const mem = std.mem;
 const testing = std.testing;
 
+const ObjectMap = @import("dynamic.zig").ObjectMap;
+const Value = @import("dynamic.zig").Value;
+
 const StringifyOptions = @import("stringify.zig").StringifyOptions;
 const stringify = @import("stringify.zig").stringify;
+const stringifyMaxDepth = @import("stringify.zig").stringifyMaxDepth;
+const stringifyArbitraryDepth = @import("stringify.zig").stringifyArbitraryDepth;
 const stringifyAlloc = @import("stringify.zig").stringifyAlloc;
+const writeStream = @import("stringify.zig").writeStream;
+const writeStreamMaxDepth = @import("stringify.zig").writeStreamMaxDepth;
+const writeStreamArbitraryDepth = @import("stringify.zig").writeStreamArbitraryDepth;
+
+test "json write stream" {
+    var out_buf: [1024]u8 = undefined;
+    var slice_stream = std.io.fixedBufferStream(&out_buf);
+    const out = slice_stream.writer();
+
+    {
+        var w = writeStream(out, .{ .whitespace = .indent_2 });
+        try testBasicWriteStream(&w, &slice_stream);
+    }
+
+    {
+        var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, 8);
+        try testBasicWriteStream(&w, &slice_stream);
+    }
+
+    {
+        var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, null);
+        try testBasicWriteStream(&w, &slice_stream);
+    }
+
+    {
+        var w = writeStreamArbitraryDepth(testing.allocator, out, .{ .whitespace = .indent_2 });
+        defer w.deinit();
+        try testBasicWriteStream(&w, &slice_stream);
+    }
+}
+
+fn testBasicWriteStream(w: anytype, slice_stream: anytype) !void {
+    slice_stream.reset();
+
+    try w.beginObject();
+
+    try w.objectField("object");
+    var arena_allocator = std.heap.ArenaAllocator.init(testing.allocator);
+    defer arena_allocator.deinit();
+    try w.write(try getJsonObject(arena_allocator.allocator()));
+
+    try w.objectField("string");
+    try w.write("This is a string");
+
+    try w.objectField("array");
+    try w.beginArray();
+    try w.write("Another string");
+    try w.write(@as(i32, 1));
+    try w.write(@as(f32, 3.5));
+    try w.endArray();
+
+    try w.objectField("int");
+    try w.write(@as(i32, 10));
+
+    try w.objectField("float");
+    try w.write(@as(f32, 3.5));
+
+    try w.endObject();
+
+    const result = slice_stream.getWritten();
+    const expected =
+        \\{
+        \\  "object": {
+        \\    "one": 1,
+        \\    "two": 2.0e+00
+        \\  },
+        \\  "string": "This is a string",
+        \\  "array": [
+        \\    "Another string",
+        \\    1,
+        \\    3.5e+00
+        \\  ],
+        \\  "int": 10,
+        \\  "float": 3.5e+00
+        \\}
+    ;
+    try std.testing.expectEqualStrings(expected, result);
+}
+
+fn getJsonObject(allocator: std.mem.Allocator) !Value {
+    var value = Value{ .object = ObjectMap.init(allocator) };
+    try value.object.put("one", Value{ .integer = @as(i64, @intCast(1)) });
+    try value.object.put("two", Value{ .float = 2.0 });
+    return value;
+}
 
 test "stringify null optional fields" {
     const MyStruct = struct {
@@ -13,64 +103,63 @@ test "stringify null optional fields" {
         another_optional: ?[]const u8 = null,
         another_required: []const u8 = "something else",
     };
-    try teststringify(
+    try testStringify(
         \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"}
     ,
         MyStruct{},
-        StringifyOptions{},
+        .{},
     );
-    try teststringify(
+    try testStringify(
         \\{"required":"something","another_required":"something else"}
     ,
         MyStruct{},
-        StringifyOptions{ .emit_null_optional_fields = false },
+        .{ .emit_null_optional_fields = false },
     );
 }
 
 test "stringify basic types" {
-    try teststringify("false", false, StringifyOptions{});
-    try teststringify("true", true, StringifyOptions{});
-    try teststringify("null", @as(?u8, null), StringifyOptions{});
-    try teststringify("null", @as(?*u32, null), StringifyOptions{});
-    try teststringify("42", 42, StringifyOptions{});
-    try teststringify("4.2e+01", 42.0, StringifyOptions{});
-    try teststringify("42", @as(u8, 42), StringifyOptions{});
-    try teststringify("42", @as(u128, 42), StringifyOptions{});
-    try teststringify("4.2e+01", @as(f32, 42), StringifyOptions{});
-    try teststringify("4.2e+01", @as(f64, 42), StringifyOptions{});
-    try teststringify("\"ItBroke\"", @as(anyerror, error.ItBroke), StringifyOptions{});
+    try testStringify("false", false, .{});
+    try testStringify("true", true, .{});
+    try testStringify("null", @as(?u8, null), .{});
+    try testStringify("null", @as(?*u32, null), .{});
+    try testStringify("42", 42, .{});
+    try testStringify("4.2e+01", 42.0, .{});
+    try testStringify("42", @as(u8, 42), .{});
+    try testStringify("42", @as(u128, 42), .{});
+    try testStringify("4.2e+01", @as(f32, 42), .{});
+    try testStringify("4.2e+01", @as(f64, 42), .{});
+    try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});
+    try testStringify("\"ItBroke\"", error.ItBroke, .{});
 }
 
 test "stringify string" {
-    try teststringify("\"hello\"", "hello", StringifyOptions{});
-    try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{});
-    try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{});
-    try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{80}\"", "with unicode\u{80}", StringifyOptions{});
-    try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", StringifyOptions{});
-    try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{100}\"", "with unicode\u{100}", StringifyOptions{});
-    try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{800}\"", "with unicode\u{800}", StringifyOptions{});
-    try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", StringifyOptions{});
-    try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", StringifyOptions{});
-    try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", StringifyOptions{});
-    try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", StringifyOptions{});
-    try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"/\"", "/", StringifyOptions{});
-    try teststringify("\"\\/\"", "/", StringifyOptions{ .string = .{ .String = .{ .escape_solidus = true } } });
+    try testStringify("\"hello\"", "hello", .{});
+    try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{});
+    try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{});
+    try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{80}\"", "with unicode\u{80}", .{});
+    try testStringify("\"with unicode\\u0080\"", "with unicode\u{80}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", .{});
+    try testStringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{100}\"", "with unicode\u{100}", .{});
+    try testStringify("\"with unicode\\u0100\"", "with unicode\u{100}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{800}\"", "with unicode\u{800}", .{});
+    try testStringify("\"with unicode\\u0800\"", "with unicode\u{800}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", .{});
+    try testStringify("\"with unicode\\u8000\"", "with unicode\u{8000}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", .{});
+    try testStringify("\"with unicode\\ud799\"", "with unicode\u{D799}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", .{});
+    try testStringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", .{ .escape_unicode = true });
+    try testStringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", .{});
+    try testStringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", .{ .escape_unicode = true });
 }
 
 test "stringify many-item sentinel-terminated string" {
-    try teststringify("\"hello\"", @as([*:0]const u8, "hello"), StringifyOptions{});
-    try teststringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
-    try teststringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } });
+    try testStringify("\"hello\"", @as([*:0]const u8, "hello"), .{});
+    try testStringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), .{ .escape_unicode = true });
+    try testStringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), .{ .escape_unicode = true });
 }
 
 test "stringify enums" {
@@ -78,8 +167,8 @@ test "stringify enums" {
         foo,
         bar,
     };
-    try teststringify("\"foo\"", E.foo, .{});
-    try teststringify("\"bar\"", E.bar, .{});
+    try testStringify("\"foo\"", E.foo, .{});
+    try testStringify("\"bar\"", E.bar, .{});
 }
 
 test "stringify tagged unions" {
@@ -88,24 +177,33 @@ test "stringify tagged unions" {
         foo: u32,
         bar: bool,
     };
-    try teststringify("{\"nothing\":{}}", T{ .nothing = {} }, StringifyOptions{});
-    try teststringify("{\"foo\":42}", T{ .foo = 42 }, StringifyOptions{});
-    try teststringify("{\"bar\":true}", T{ .bar = true }, StringifyOptions{});
+    try testStringify("{\"nothing\":{}}", T{ .nothing = {} }, .{});
+    try testStringify("{\"foo\":42}", T{ .foo = 42 }, .{});
+    try testStringify("{\"bar\":true}", T{ .bar = true }, .{});
 }
 
 test "stringify struct" {
-    try teststringify("{\"foo\":42}", struct {
+    try testStringify("{\"foo\":42}", struct {
         foo: u32,
-    }{ .foo = 42 }, StringifyOptions{});
+    }{ .foo = 42 }, .{});
 }
 
-test "stringify struct with string as array" {
-    try teststringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, StringifyOptions{});
-    try teststringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, StringifyOptions{ .string = .Array });
+test "emit_strings_as_arrays" {
+    // Should only affect string values, not object keys.
+    try testStringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, .{});
+    try testStringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, .{ .emit_strings_as_arrays = true });
+    // Should *not* affect these types:
+    try testStringify("\"foo\"", @as(enum { foo, bar }, .foo), .{ .emit_strings_as_arrays = true });
+    try testStringify("\"ItBroke\"", error.ItBroke, .{ .emit_strings_as_arrays = true });
+    // Should work on these:
+    try testStringify("\"bar\"", @Vector(3, u8){ 'b', 'a', 'r' }, .{});
+    try testStringify("[98,97,114]", @Vector(3, u8){ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
+    try testStringify("\"bar\"", [3]u8{ 'b', 'a', 'r' }, .{});
+    try testStringify("[98,97,114]", [3]u8{ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
 }
 
 test "stringify struct with indentation" {
-    try teststringify(
+    try testStringify(
         \\{
         \\    "foo": 42,
         \\    "bar": [
@@ -122,12 +220,10 @@ test "stringify struct with indentation" {
             .foo = 42,
             .bar = .{ 1, 2, 3 },
         },
-        StringifyOptions{
-            .whitespace = .{},
-        },
+        .{ .whitespace = .indent_4 },
     );
-    try teststringify(
-        "{\n\t\"foo\":42,\n\t\"bar\":[\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}",
+    try testStringify(
+        "{\n\t\"foo\": 42,\n\t\"bar\": [\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}",
         struct {
             foo: u32,
             bar: [3]u32,
@@ -135,14 +231,9 @@ test "stringify struct with indentation" {
             .foo = 42,
             .bar = .{ 1, 2, 3 },
         },
-        StringifyOptions{
-            .whitespace = .{
-                .indent = .tab,
-                .separator = false,
-            },
-        },
+        .{ .whitespace = .indent_tab },
     );
-    try teststringify(
+    try testStringify(
         \\{"foo":42,"bar":[1,2,3]}
     ,
         struct {
@@ -152,59 +243,53 @@ test "stringify struct with indentation" {
             .foo = 42,
             .bar = .{ 1, 2, 3 },
         },
-        StringifyOptions{
-            .whitespace = .{
-                .indent = .none,
-                .separator = false,
-            },
-        },
+        .{ .whitespace = .minified },
     );
 }
 
 test "stringify struct with void field" {
-    try teststringify("{\"foo\":42}", struct {
+    try testStringify("{\"foo\":42}", struct {
         foo: u32,
         bar: void = {},
-    }{ .foo = 42 }, StringifyOptions{});
+    }{ .foo = 42 }, .{});
 }
 
 test "stringify array of structs" {
     const MyStruct = struct {
         foo: u32,
     };
-    try teststringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
+    try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
         MyStruct{ .foo = 42 },
         MyStruct{ .foo = 100 },
         MyStruct{ .foo = 1000 },
-    }, StringifyOptions{});
+    }, .{});
 }
 
 test "stringify struct with custom stringifier" {
-    try teststringify("[\"something special\",42]", struct {
+    try testStringify("[\"something special\",42]", struct {
         foo: u32,
         const Self = @This();
-        pub fn jsonStringify(
-            value: Self,
-            options: StringifyOptions,
-            out_stream: anytype,
-        ) !void {
+        pub fn jsonStringify(value: @This(), jws: anytype) !void {
             _ = value;
-            try out_stream.writeAll("[\"something special\",");
-            try stringify(42, options, out_stream);
-            try out_stream.writeByte(']');
+            try jws.beginArray();
+            try jws.write("something special");
+            try jws.write(42);
+            try jws.endArray();
         }
-    }{ .foo = 42 }, StringifyOptions{});
+    }{ .foo = 42 }, .{});
 }
 
 test "stringify vector" {
-    try teststringify("[1,1]", @as(@Vector(2, u32), @splat(1)), StringifyOptions{});
+    try testStringify("[1,1]", @as(@Vector(2, u32), @splat(1)), .{});
+    try testStringify("\"AA\"", @as(@Vector(2, u8), @splat('A')), .{});
+    try testStringify("[65,65]", @as(@Vector(2, u8), @splat('A')), .{ .emit_strings_as_arrays = true });
 }
 
 test "stringify tuple" {
-    try teststringify("[\"foo\",42]", std.meta.Tuple(&.{ []const u8, usize }){ "foo", 42 }, StringifyOptions{});
+    try testStringify("[\"foo\",42]", std.meta.Tuple(&.{ []const u8, usize }){ "foo", 42 }, .{});
 }
 
-fn teststringify(expected: []const u8, value: anytype, options: StringifyOptions) !void {
+fn testStringify(expected: []const u8, value: anytype, options: StringifyOptions) !void {
     const ValidationWriter = struct {
         const Self = @This();
         pub const Writer = std.io.Writer(*Self, Error, write);
@@ -256,8 +341,34 @@ fn teststringify(expected: []const u8, value: anytype, options: StringifyOptions
     };
 
     var vos = ValidationWriter.init(expected);
-    try stringify(value, options, vos.writer());
+    try stringifyArbitraryDepth(testing.allocator, value, options, vos.writer());
     if (vos.expected_remaining.len > 0) return error.NotEnoughData;
+
+    // Also test with safety disabled.
+    try testStringifyMaxDepth(expected, value, options, null);
+    try testStringifyArbitraryDepth(expected, value, options);
+}
+
+fn testStringifyMaxDepth(expected: []const u8, value: anytype, options: StringifyOptions, comptime max_depth: ?usize) !void {
+    var out_buf: [1024]u8 = undefined;
+    var slice_stream = std.io.fixedBufferStream(&out_buf);
+    const out = slice_stream.writer();
+
+    try stringifyMaxDepth(value, options, out, max_depth);
+    const got = slice_stream.getWritten();
+
+    try testing.expectEqualStrings(expected, got);
+}
+
+fn testStringifyArbitraryDepth(expected: []const u8, value: anytype, options: StringifyOptions) !void {
+    var out_buf: [1024]u8 = undefined;
+    var slice_stream = std.io.fixedBufferStream(&out_buf);
+    const out = slice_stream.writer();
+
+    try stringifyArbitraryDepth(testing.allocator, value, options, out);
+    const got = slice_stream.getWritten();
+
+    try testing.expectEqualStrings(expected, got);
 }
 
 test "stringify alloc" {
@@ -270,3 +381,54 @@ test "stringify alloc" {
 
     try std.testing.expectEqualStrings(expected, actual);
 }
+
+test "comptime stringify" {
+    comptime testStringifyMaxDepth("false", false, .{}, null) catch unreachable;
+    comptime testStringifyMaxDepth("false", false, .{}, 0) catch unreachable;
+    comptime testStringifyArbitraryDepth("false", false, .{}) catch unreachable;
+
+    const MyStruct = struct {
+        foo: u32,
+    };
+    comptime testStringifyMaxDepth("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
+        MyStruct{ .foo = 42 },
+        MyStruct{ .foo = 100 },
+        MyStruct{ .foo = 1000 },
+    }, .{}, null) catch unreachable;
+    comptime testStringifyMaxDepth("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
+        MyStruct{ .foo = 42 },
+        MyStruct{ .foo = 100 },
+        MyStruct{ .foo = 1000 },
+    }, .{}, 8) catch unreachable;
+}
+
+test "writePreformatted" {
+    var out_buf: [1024]u8 = undefined;
+    var slice_stream = std.io.fixedBufferStream(&out_buf);
+    const out = slice_stream.writer();
+
+    var w = writeStream(out, .{ .whitespace = .indent_2 });
+    defer w.deinit();
+
+    try w.beginObject();
+    try w.objectField("a");
+    try w.writePreformatted("[  ]");
+    try w.objectField("b");
+    try w.beginArray();
+    try w.writePreformatted("[[]] ");
+    try w.writePreformatted("  {}");
+    try w.endArray();
+    try w.endObject();
+
+    const result = slice_stream.getWritten();
+    const expected =
+        \\{
+        \\  "a": [  ],
+        \\  "b": [
+        \\    [[]] ,
+        \\      {}
+        \\  ]
+        \\}
+    ;
+    try std.testing.expectEqualStrings(expected, result);
+}
lib/std/json/test.zig
@@ -4,6 +4,7 @@ const parseFromSlice = @import("./static.zig").parseFromSlice;
 const validate = @import("./scanner.zig").validate;
 const JsonScanner = @import("./scanner.zig").Scanner;
 const Value = @import("./dynamic.zig").Value;
+const stringifyAlloc = @import("./stringify.zig").stringifyAlloc;
 
 // Support for JSONTestSuite.zig
 pub fn ok(s: []const u8) !void {
@@ -49,11 +50,10 @@ fn roundTrip(s: []const u8) !void {
     var parsed = try parseFromSlice(Value, testing.allocator, s, .{});
     defer parsed.deinit();
 
-    var buf: [256]u8 = undefined;
-    var fbs = std.io.fixedBufferStream(&buf);
-    try parsed.value.jsonStringify(.{}, fbs.writer());
+    const rendered = try stringifyAlloc(testing.allocator, parsed.value, .{});
+    defer testing.allocator.free(rendered);
 
-    try testing.expectEqualStrings(s, fbs.getWritten());
+    try testing.expectEqualStrings(s, rendered);
 }
 
 test "truncated UTF-8 sequence" {
lib/std/json/write_stream.zig
@@ -1,300 +0,0 @@
-const std = @import("std");
-const assert = std.debug.assert;
-const maxInt = std.math.maxInt;
-
-const StringifyOptions = @import("./stringify.zig").StringifyOptions;
-const jsonStringify = @import("./stringify.zig").stringify;
-
-const Value = @import("./dynamic.zig").Value;
-
-const State = enum {
-    complete,
-    value,
-    array_start,
-    array,
-    object_start,
-    object,
-};
-
-/// Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data
-/// to a stream. `max_depth` is a comptime-known upper bound on the nesting depth.
-/// TODO A future iteration of this API will allow passing `null` for this value,
-/// and disable safety checks in release builds.
-pub fn WriteStream(comptime OutStream: type, comptime max_depth: usize) type {
-    return struct {
-        const Self = @This();
-
-        pub const Stream = OutStream;
-
-        whitespace: StringifyOptions.Whitespace = StringifyOptions.Whitespace{
-            .indent_level = 0,
-            .indent = .{ .space = 1 },
-        },
-
-        stream: OutStream,
-        state_index: usize,
-        state: [max_depth]State,
-
-        pub fn init(stream: OutStream) Self {
-            var self = Self{
-                .stream = stream,
-                .state_index = 1,
-                .state = undefined,
-            };
-            self.state[0] = .complete;
-            self.state[1] = .value;
-            return self;
-        }
-
-        pub fn beginArray(self: *Self) !void {
-            assert(self.state[self.state_index] == State.value); // need to call arrayElem or objectField
-            try self.stream.writeByte('[');
-            self.state[self.state_index] = State.array_start;
-            self.whitespace.indent_level += 1;
-        }
-
-        pub fn beginObject(self: *Self) !void {
-            assert(self.state[self.state_index] == State.value); // need to call arrayElem or objectField
-            try self.stream.writeByte('{');
-            self.state[self.state_index] = State.object_start;
-            self.whitespace.indent_level += 1;
-        }
-
-        pub fn arrayElem(self: *Self) !void {
-            const state = self.state[self.state_index];
-            switch (state) {
-                .complete => unreachable,
-                .value => unreachable,
-                .object_start => unreachable,
-                .object => unreachable,
-                .array, .array_start => {
-                    if (state == .array) {
-                        try self.stream.writeByte(',');
-                    }
-                    self.state[self.state_index] = .array;
-                    self.pushState(.value);
-                    try self.indent();
-                },
-            }
-        }
-
-        pub fn objectField(self: *Self, name: []const u8) !void {
-            const state = self.state[self.state_index];
-            switch (state) {
-                .complete => unreachable,
-                .value => unreachable,
-                .array_start => unreachable,
-                .array => unreachable,
-                .object, .object_start => {
-                    if (state == .object) {
-                        try self.stream.writeByte(',');
-                    }
-                    self.state[self.state_index] = .object;
-                    self.pushState(.value);
-                    try self.indent();
-                    try self.writeEscapedString(name);
-                    try self.stream.writeByte(':');
-                    if (self.whitespace.separator) {
-                        try self.stream.writeByte(' ');
-                    }
-                },
-            }
-        }
-
-        pub fn endArray(self: *Self) !void {
-            switch (self.state[self.state_index]) {
-                .complete => unreachable,
-                .value => unreachable,
-                .object_start => unreachable,
-                .object => unreachable,
-                .array_start => {
-                    self.whitespace.indent_level -= 1;
-                    try self.stream.writeByte(']');
-                    self.popState();
-                },
-                .array => {
-                    self.whitespace.indent_level -= 1;
-                    try self.indent();
-                    self.popState();
-                    try self.stream.writeByte(']');
-                },
-            }
-        }
-
-        pub fn endObject(self: *Self) !void {
-            switch (self.state[self.state_index]) {
-                .complete => unreachable,
-                .value => unreachable,
-                .array_start => unreachable,
-                .array => unreachable,
-                .object_start => {
-                    self.whitespace.indent_level -= 1;
-                    try self.stream.writeByte('}');
-                    self.popState();
-                },
-                .object => {
-                    self.whitespace.indent_level -= 1;
-                    try self.indent();
-                    self.popState();
-                    try self.stream.writeByte('}');
-                },
-            }
-        }
-
-        pub fn emitNull(self: *Self) !void {
-            assert(self.state[self.state_index] == State.value);
-            try self.stringify(null);
-            self.popState();
-        }
-
-        pub fn emitBool(self: *Self, value: bool) !void {
-            assert(self.state[self.state_index] == State.value);
-            try self.stringify(value);
-            self.popState();
-        }
-
-        pub fn emitNumber(
-            self: *Self,
-            /// An integer, float, or `std.math.BigInt`. Emitted as a bare number if it fits losslessly
-            /// in a IEEE 754 double float, otherwise emitted as a string to the full precision.
-            value: anytype,
-        ) !void {
-            assert(self.state[self.state_index] == State.value);
-            switch (@typeInfo(@TypeOf(value))) {
-                .Int => |info| {
-                    if (info.bits < 53) {
-                        try self.stream.print("{}", .{value});
-                        self.popState();
-                        return;
-                    }
-                    if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) {
-                        try self.stream.print("{}", .{value});
-                        self.popState();
-                        return;
-                    }
-                },
-                .ComptimeInt => {
-                    return self.emitNumber(@as(std.math.IntFittingRange(value, value), value));
-                },
-                .Float, .ComptimeFloat => if (@as(f64, @floatCast(value)) == value) {
-                    try self.stream.print("{}", .{@as(f64, @floatCast(value))});
-                    self.popState();
-                    return;
-                },
-                else => {},
-            }
-            try self.stream.print("\"{}\"", .{value});
-            self.popState();
-        }
-
-        pub fn emitString(self: *Self, string: []const u8) !void {
-            assert(self.state[self.state_index] == State.value);
-            try self.writeEscapedString(string);
-            self.popState();
-        }
-
-        fn writeEscapedString(self: *Self, string: []const u8) !void {
-            assert(std.unicode.utf8ValidateSlice(string));
-            try self.stringify(string);
-        }
-
-        /// Writes the complete json into the output stream
-        pub fn emitJson(self: *Self, value: Value) Stream.Error!void {
-            assert(self.state[self.state_index] == State.value);
-            try self.stringify(value);
-            self.popState();
-        }
-
-        fn indent(self: *Self) !void {
-            assert(self.state_index >= 1);
-            try self.whitespace.outputIndent(self.stream);
-        }
-
-        fn pushState(self: *Self, state: State) void {
-            self.state_index += 1;
-            self.state[self.state_index] = state;
-        }
-
-        fn popState(self: *Self) void {
-            self.state_index -= 1;
-        }
-
-        fn stringify(self: *Self, value: anytype) !void {
-            try jsonStringify(value, StringifyOptions{
-                .whitespace = self.whitespace,
-            }, self.stream);
-        }
-    };
-}
-
-pub fn writeStream(
-    out_stream: anytype,
-    comptime max_depth: usize,
-) WriteStream(@TypeOf(out_stream), max_depth) {
-    return WriteStream(@TypeOf(out_stream), max_depth).init(out_stream);
-}
-
-const ObjectMap = @import("./dynamic.zig").ObjectMap;
-
-test "json write stream" {
-    var out_buf: [1024]u8 = undefined;
-    var slice_stream = std.io.fixedBufferStream(&out_buf);
-    const out = slice_stream.writer();
-
-    var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
-    defer arena_allocator.deinit();
-
-    var w = writeStream(out, 10);
-
-    try w.beginObject();
-
-    try w.objectField("object");
-    try w.emitJson(try getJsonObject(arena_allocator.allocator()));
-
-    try w.objectField("string");
-    try w.emitString("This is a string");
-
-    try w.objectField("array");
-    try w.beginArray();
-    try w.arrayElem();
-    try w.emitString("Another string");
-    try w.arrayElem();
-    try w.emitNumber(@as(i32, 1));
-    try w.arrayElem();
-    try w.emitNumber(@as(f32, 3.5));
-    try w.endArray();
-
-    try w.objectField("int");
-    try w.emitNumber(@as(i32, 10));
-
-    try w.objectField("float");
-    try w.emitNumber(@as(f32, 3.5));
-
-    try w.endObject();
-
-    const result = slice_stream.getWritten();
-    const expected =
-        \\{
-        \\ "object": {
-        \\  "one": 1,
-        \\  "two": 2.0e+00
-        \\ },
-        \\ "string": "This is a string",
-        \\ "array": [
-        \\  "Another string",
-        \\  1,
-        \\  3.5e+00
-        \\ ],
-        \\ "int": 10,
-        \\ "float": 3.5e+00
-        \\}
-    ;
-    try std.testing.expect(std.mem.eql(u8, expected, result));
-}
-
-fn getJsonObject(allocator: std.mem.Allocator) !Value {
-    var value = Value{ .object = ObjectMap.init(allocator) };
-    try value.object.put("one", Value{ .integer = @as(i64, @intCast(1)) });
-    try value.object.put("two", Value{ .float = 2.0 });
-    return value;
-}
lib/std/BitStack.zig
@@ -0,0 +1,86 @@
+//! Effectively a stack of u1 values implemented using ArrayList(u8).
+
+const BitStack = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArrayList = std.ArrayList;
+
+bytes: std.ArrayList(u8),
+bit_len: usize = 0,
+
+pub fn init(allocator: Allocator) @This() {
+    return .{
+        .bytes = std.ArrayList(u8).init(allocator),
+    };
+}
+
+pub fn deinit(self: *@This()) void {
+    self.bytes.deinit();
+    self.* = undefined;
+}
+
+pub fn ensureTotalCapacity(self: *@This(), bit_capcity: usize) Allocator.Error!void {
+    const byte_capacity = (bit_capcity + 7) >> 3;
+    try self.bytes.ensureTotalCapacity(byte_capacity);
+}
+
+pub fn push(self: *@This(), b: u1) Allocator.Error!void {
+    const byte_index = self.bit_len >> 3;
+    if (self.bytes.items.len <= byte_index) {
+        try self.bytes.append(0);
+    }
+
+    pushWithStateAssumeCapacity(self.bytes.items, &self.bit_len, b);
+}
+
+pub fn peek(self: *const @This()) u1 {
+    return peekWithState(self.bytes.items, self.bit_len);
+}
+
+pub fn pop(self: *@This()) u1 {
+    return popWithState(self.bytes.items, &self.bit_len);
+}
+
+/// Standalone function for working with a fixed-size buffer.
+pub fn pushWithStateAssumeCapacity(buf: []u8, bit_len: *usize, b: u1) void {
+    const byte_index = bit_len.* >> 3;
+    const bit_index = @as(u3, @intCast(bit_len.* & 7));
+
+    buf[byte_index] &= ~(@as(u8, 1) << bit_index);
+    buf[byte_index] |= @as(u8, b) << bit_index;
+
+    bit_len.* += 1;
+}
+
+/// Standalone function for working with a fixed-size buffer.
+pub fn peekWithState(buf: []const u8, bit_len: usize) u1 {
+    const byte_index = (bit_len - 1) >> 3;
+    const bit_index = @as(u3, @intCast((bit_len - 1) & 7));
+    return @as(u1, @intCast((buf[byte_index] >> bit_index) & 1));
+}
+
+/// Standalone function for working with a fixed-size buffer.
+pub fn popWithState(buf: []const u8, bit_len: *usize) u1 {
+    const b = peekWithState(buf, bit_len.*);
+    bit_len.* -= 1;
+    return b;
+}
+
+const testing = std.testing;
+test BitStack {
+    var stack = BitStack.init(testing.allocator);
+    defer stack.deinit();
+
+    try stack.push(1);
+    try stack.push(0);
+    try stack.push(0);
+    try stack.push(1);
+
+    try testing.expectEqual(@as(u1, 1), stack.peek());
+    try testing.expectEqual(@as(u1, 1), stack.pop());
+    try testing.expectEqual(@as(u1, 0), stack.peek());
+    try testing.expectEqual(@as(u1, 0), stack.pop());
+    try testing.expectEqual(@as(u1, 0), stack.pop());
+    try testing.expectEqual(@as(u1, 1), stack.pop());
+}
lib/std/json.zig
@@ -43,14 +43,15 @@ test Value {
 test writeStream {
     var out = ArrayList(u8).init(testing.allocator);
     defer out.deinit();
-    var write_stream = writeStream(out.writer(), 99);
+    var write_stream = writeStream(out.writer(), .{ .whitespace = .indent_2 });
+    defer write_stream.deinit();
     try write_stream.beginObject();
     try write_stream.objectField("foo");
-    try write_stream.emitNumber(123);
+    try write_stream.write(123);
     try write_stream.endObject();
     const expected =
         \\{
-        \\ "foo": 123
+        \\  "foo": 123
         \\}
     ;
     try testing.expectEqualSlices(u8, expected, out.items);
@@ -98,13 +99,16 @@ pub const ParseError = @import("json/static.zig").ParseError;
 pub const ParseFromValueError = @import("json/static.zig").ParseFromValueError;
 
 pub const StringifyOptions = @import("json/stringify.zig").StringifyOptions;
-pub const encodeJsonString = @import("json/stringify.zig").encodeJsonString;
-pub const encodeJsonStringChars = @import("json/stringify.zig").encodeJsonStringChars;
 pub const stringify = @import("json/stringify.zig").stringify;
+pub const stringifyMaxDepth = @import("json/stringify.zig").stringifyMaxDepth;
+pub const stringifyArbitraryDepth = @import("json/stringify.zig").stringifyArbitraryDepth;
 pub const stringifyAlloc = @import("json/stringify.zig").stringifyAlloc;
-
-pub const WriteStream = @import("json/write_stream.zig").WriteStream;
-pub const writeStream = @import("json/write_stream.zig").writeStream;
+pub const writeStream = @import("json/stringify.zig").writeStream;
+pub const writeStreamMaxDepth = @import("json/stringify.zig").writeStreamMaxDepth;
+pub const writeStreamArbitraryDepth = @import("json/stringify.zig").writeStreamArbitraryDepth;
+pub const WriteStream = @import("json/stringify.zig").WriteStream;
+pub const encodeJsonString = @import("json/stringify.zig").encodeJsonString;
+pub const encodeJsonStringChars = @import("json/stringify.zig").encodeJsonStringChars;
 
 // Deprecations
 pub const parse = @compileError("Deprecated; use parseFromSlice() or parseFromTokenSource() instead.");
@@ -117,9 +121,8 @@ pub const TokenStream = @compileError("Deprecated; use json.Scanner or json.Read
 test {
     _ = @import("json/test.zig");
     _ = @import("json/scanner.zig");
-    _ = @import("json/write_stream.zig");
     _ = @import("json/dynamic.zig");
-    _ = @import("json/hashmap_test.zig");
+    _ = @import("json/hashmap.zig");
     _ = @import("json/static.zig");
     _ = @import("json/stringify.zig");
     _ = @import("json/JSONTestSuite_test.zig");
lib/std/std.zig
@@ -8,6 +8,7 @@ pub const AutoArrayHashMap = array_hash_map.AutoArrayHashMap;
 pub const AutoArrayHashMapUnmanaged = array_hash_map.AutoArrayHashMapUnmanaged;
 pub const AutoHashMap = hash_map.AutoHashMap;
 pub const AutoHashMapUnmanaged = hash_map.AutoHashMapUnmanaged;
+pub const BitStack = @import("BitStack.zig");
 pub const BoundedArray = @import("bounded_array.zig").BoundedArray;
 pub const BoundedArrayAligned = @import("bounded_array.zig").BoundedArrayAligned;
 pub const Build = @import("Build.zig");
src/Autodoc.zig
@@ -385,10 +385,11 @@ pub fn generateZirData(self: *Autodoc) !void {
             \\ /** @type {{DocData}} */
             \\ var zigAnalysis=
         , .{});
-        try std.json.stringify(
+        try std.json.stringifyArbitraryDepth(
+            arena_allocator.allocator(),
             data,
             .{
-                .whitespace = .{ .indent = .none, .separator = false },
+                .whitespace = .minified,
                 .emit_null_optional_fields = true,
             },
             out,
@@ -532,28 +533,16 @@ const DocData = struct {
         ret: Expr,
     };
 
-    pub fn jsonStringify(
-        self: DocData,
-        opts: std.json.StringifyOptions,
-        w: anytype,
-    ) !void {
-        var jsw = std.json.writeStream(w, 15);
-        jsw.whitespace = opts.whitespace;
+    pub fn jsonStringify(self: DocData, jsw: anytype) !void {
         try jsw.beginObject();
         inline for (comptime std.meta.tags(std.meta.FieldEnum(DocData))) |f| {
             const f_name = @tagName(f);
             try jsw.objectField(f_name);
             switch (f) {
-                .files => try writeFileTableToJson(self.files, self.modules, &jsw),
-                .guide_sections => try writeGuidesToJson(self.guide_sections, &jsw),
-                .modules => {
-                    try std.json.stringify(self.modules.values(), opts, w);
-                    jsw.state_index -= 1;
-                },
-                else => {
-                    try std.json.stringify(@field(self, f_name), opts, w);
-                    jsw.state_index -= 1;
-                },
+                .files => try writeFileTableToJson(self.files, self.modules, jsw),
+                .guide_sections => try writeGuidesToJson(self.guide_sections, jsw),
+                .modules => try jsw.write(self.modules.values()),
+                else => try jsw.write(@field(self, f_name)),
             }
         }
         try jsw.endObject();
@@ -583,24 +572,14 @@ const DocData = struct {
             value: usize,
         };
 
-        pub fn jsonStringify(
-            self: DocModule,
-            opts: std.json.StringifyOptions,
-            w: anytype,
-        ) !void {
-            var jsw = std.json.writeStream(w, 15);
-            jsw.whitespace = opts.whitespace;
-
+        pub fn jsonStringify(self: DocModule, jsw: anytype) !void {
             try jsw.beginObject();
             inline for (comptime std.meta.tags(std.meta.FieldEnum(DocModule))) |f| {
                 const f_name = @tagName(f);
                 try jsw.objectField(f_name);
                 switch (f) {
-                    .table => try writeModuleTableToJson(self.table, &jsw),
-                    else => {
-                        try std.json.stringify(@field(self, f_name), opts, w);
-                        jsw.state_index -= 1;
-                    },
+                    .table => try writeModuleTableToJson(self.table, jsw),
+                    else => try jsw.write(@field(self, f_name)),
                 }
             }
             try jsw.endObject();
@@ -617,18 +596,10 @@ const DocData = struct {
         is_uns: bool = false, // usingnamespace
         parent_container: ?usize, // index into `types`
 
-        pub fn jsonStringify(
-            self: Decl,
-            opts: std.json.StringifyOptions,
-            w: anytype,
-        ) !void {
-            var jsw = std.json.writeStream(w, 15);
-            jsw.whitespace = opts.whitespace;
+        pub fn jsonStringify(self: Decl, jsw: anytype) !void {
             try jsw.beginArray();
             inline for (comptime std.meta.fields(Decl)) |f| {
-                try jsw.arrayElem();
-                try std.json.stringify(@field(self, f.name), opts, w);
-                jsw.state_index -= 1;
+                try jsw.write(@field(self, f.name));
             }
             try jsw.endArray();
         }
@@ -644,18 +615,10 @@ const DocData = struct {
         fields: ?[]usize = null, // index into astNodes
         @"comptime": bool = false,
 
-        pub fn jsonStringify(
-            self: AstNode,
-            opts: std.json.StringifyOptions,
-            w: anytype,
-        ) !void {
-            var jsw = std.json.writeStream(w, 15);
-            jsw.whitespace = opts.whitespace;
+        pub fn jsonStringify(self: AstNode, jsw: anytype) !void {
             try jsw.beginArray();
             inline for (comptime std.meta.fields(AstNode)) |f| {
-                try jsw.arrayElem();
-                try std.json.stringify(@field(self, f.name), opts, w);
-                jsw.state_index -= 1;
+                try jsw.write(@field(self, f.name));
             }
             try jsw.endArray();
         }
@@ -776,27 +739,18 @@ const DocData = struct {
             docs: []const u8,
         };
 
-        pub fn jsonStringify(
-            self: Type,
-            opts: std.json.StringifyOptions,
-            w: anytype,
-        ) !void {
+        pub fn jsonStringify(self: Type, jsw: anytype) !void {
             const active_tag = std.meta.activeTag(self);
-            var jsw = std.json.writeStream(w, 15);
-            jsw.whitespace = opts.whitespace;
             try jsw.beginArray();
-            try jsw.arrayElem();
-            try jsw.emitNumber(@intFromEnum(active_tag));
+            try jsw.write(@intFromEnum(active_tag));
             inline for (comptime std.meta.fields(Type)) |case| {
                 if (@field(Type, case.name) == active_tag) {
                     const current_value = @field(self, case.name);
                     inline for (comptime std.meta.fields(case.type)) |f| {
-                        try jsw.arrayElem();
                         if (f.type == std.builtin.Type.Pointer.Size) {
-                            try jsw.emitNumber(@intFromEnum(@field(current_value, f.name)));
+                            try jsw.write(@intFromEnum(@field(current_value, f.name)));
                         } else {
-                            try std.json.stringify(@field(current_value, f.name), opts, w);
-                            jsw.state_index -= 1;
+                            try jsw.write(@field(current_value, f.name));
                         }
                     }
                 }
@@ -919,14 +873,8 @@ const DocData = struct {
             val: WalkResult,
         };
 
-        pub fn jsonStringify(
-            self: Expr,
-            opts: std.json.StringifyOptions,
-            w: anytype,
-        ) @TypeOf(w).Error!void {
+        pub fn jsonStringify(self: Expr, jsw: anytype) !void {
             const active_tag = std.meta.activeTag(self);
-            var jsw = std.json.writeStream(w, 15);
-            jsw.whitespace = opts.whitespace;
             try jsw.beginObject();
             if (active_tag == .declIndex) {
                 try jsw.objectField("declRef");
@@ -935,14 +883,17 @@ const DocData = struct {
             }
             switch (self) {
                 .int => {
-                    if (self.int.negated) try w.writeAll("-");
-                    try jsw.emitNumber(self.int.value);
+                    if (self.int.negated) {
+                        try jsw.write(-@as(i65, self.int.value));
+                    } else {
+                        try jsw.write(self.int.value);
+                    }
                 },
                 .builtinField => {
-                    try jsw.emitString(@tagName(self.builtinField));
+                    try jsw.write(@tagName(self.builtinField));
                 },
                 .declRef => {
-                    try jsw.emitNumber(self.declRef.Analyzed);
+                    try jsw.write(self.declRef.Analyzed);
                 },
                 else => {
                     inline for (comptime std.meta.fields(Expr)) |case| {
@@ -952,14 +903,7 @@ const DocData = struct {
                         if (comptime std.mem.eql(u8, case.name, "declRef"))
                             continue;
                         if (@field(Expr, case.name) == active_tag) {
-                            try std.json.stringify(@field(self, case.name), opts, w);
-                            jsw.state_index -= 1;
-                            // TODO: we should not reach into the state of the
-                            //       json writer, but alas, this is what's
-                            //       necessary with the current api.
-                            //       would be nice to have a proper integration
-                            //       between the json writer and the generic
-                            //       std.json.stringify implementation
+                            try jsw.write(@field(self, case.name));
                         }
                     }
                 },
@@ -5440,12 +5384,9 @@ fn writeFileTableToJson(
     try jsw.beginArray();
     var it = map.iterator();
     while (it.next()) |entry| {
-        try jsw.arrayElem();
         try jsw.beginArray();
-        try jsw.arrayElem();
-        try jsw.emitString(entry.key_ptr.*.sub_file_path);
-        try jsw.arrayElem();
-        try jsw.emitNumber(mods.getIndex(entry.key_ptr.*.pkg) orelse 0);
+        try jsw.write(entry.key_ptr.*.sub_file_path);
+        try jsw.write(mods.getIndex(entry.key_ptr.*.pkg) orelse 0);
         try jsw.endArray();
     }
     try jsw.endArray();
@@ -5462,21 +5403,19 @@ fn writeGuidesToJson(sections: std.ArrayListUnmanaged(Section), jsw: anytype) !v
 
     for (sections.items) |s| {
         // section name
-        try jsw.arrayElem();
         try jsw.beginObject();
         try jsw.objectField("name");
-        try jsw.emitString(s.name);
+        try jsw.write(s.name);
         try jsw.objectField("guides");
 
         // section value
         try jsw.beginArray();
         for (s.guides.items) |g| {
-            try jsw.arrayElem();
             try jsw.beginObject();
             try jsw.objectField("name");
-            try jsw.emitString(g.name);
+            try jsw.write(g.name);
             try jsw.objectField("body");
-            try jsw.emitString(g.body);
+            try jsw.write(g.body);
             try jsw.endObject();
         }
         try jsw.endArray();
@@ -5494,7 +5433,7 @@ fn writeModuleTableToJson(
     var it = map.valueIterator();
     while (it.next()) |entry| {
         try jsw.objectField(entry.name);
-        try jsw.emitNumber(entry.value);
+        try jsw.write(entry.value);
     }
     try jsw.endObject();
 }
src/print_env.zig
@@ -28,26 +28,27 @@ pub fn cmdEnv(gpa: Allocator, args: []const []const u8, stdout: std.fs.File.Writ
     var bw = std.io.bufferedWriter(stdout);
     const w = bw.writer();
 
-    var jws = std.json.writeStream(w, 6);
+    var jws = std.json.writeStream(w, .{ .whitespace = .indent_1 });
+
     try jws.beginObject();
 
     try jws.objectField("zig_exe");
-    try jws.emitString(self_exe_path);
+    try jws.write(self_exe_path);
 
     try jws.objectField("lib_dir");
-    try jws.emitString(zig_lib_directory.path.?);
+    try jws.write(zig_lib_directory.path.?);
 
     try jws.objectField("std_dir");
-    try jws.emitString(zig_std_dir);
+    try jws.write(zig_std_dir);
 
     try jws.objectField("global_cache_dir");
-    try jws.emitString(global_cache_dir);
+    try jws.write(global_cache_dir);
 
     try jws.objectField("version");
-    try jws.emitString(build_options.version);
+    try jws.write(build_options.version);
 
     try jws.objectField("target");
-    try jws.emitString(triple);
+    try jws.write(triple);
 
     try jws.endObject();
     try w.writeByte('\n');
src/print_targets.zig
@@ -40,31 +40,28 @@ pub fn cmdTargets(
 
     var bw = io.bufferedWriter(stdout);
     const w = bw.writer();
-    var jws = std.json.writeStream(w, 6);
+    var jws = std.json.writeStream(w, .{ .whitespace = .indent_1 });
 
     try jws.beginObject();
 
     try jws.objectField("arch");
     try jws.beginArray();
     for (meta.fieldNames(Target.Cpu.Arch)) |field| {
-        try jws.arrayElem();
-        try jws.emitString(field);
+        try jws.write(field);
     }
     try jws.endArray();
 
     try jws.objectField("os");
     try jws.beginArray();
     for (meta.fieldNames(Target.Os.Tag)) |field| {
-        try jws.arrayElem();
-        try jws.emitString(field);
+        try jws.write(field);
     }
     try jws.endArray();
 
     try jws.objectField("abi");
     try jws.beginArray();
     for (meta.fieldNames(Target.Abi)) |field| {
-        try jws.arrayElem();
-        try jws.emitString(field);
+        try jws.write(field);
     }
     try jws.endArray();
 
@@ -75,19 +72,16 @@ pub fn cmdTargets(
             @tagName(libc.arch), @tagName(libc.os), @tagName(libc.abi),
         });
         defer allocator.free(tmp);
-        try jws.arrayElem();
-        try jws.emitString(tmp);
+        try jws.write(tmp);
     }
     try jws.endArray();
 
     try jws.objectField("glibc");
     try jws.beginArray();
     for (glibc_abi.all_versions) |ver| {
-        try jws.arrayElem();
-
         const tmp = try std.fmt.allocPrint(allocator, "{}", .{ver});
         defer allocator.free(tmp);
-        try jws.emitString(tmp);
+        try jws.write(tmp);
     }
     try jws.endArray();
 
@@ -102,8 +96,7 @@ pub fn cmdTargets(
             for (arch.allFeaturesList(), 0..) |feature, i_usize| {
                 const index = @as(Target.Cpu.Feature.Set.Index, @intCast(i_usize));
                 if (model.features.isEnabled(index)) {
-                    try jws.arrayElem();
-                    try jws.emitString(feature.name);
+                    try jws.write(feature.name);
                 }
             }
             try jws.endArray();
@@ -118,8 +111,7 @@ pub fn cmdTargets(
         try jws.objectField(@tagName(arch));
         try jws.beginArray();
         for (arch.allFeaturesList()) |feature| {
-            try jws.arrayElem();
-            try jws.emitString(feature.name);
+            try jws.write(feature.name);
         }
         try jws.endArray();
     }
@@ -131,17 +123,17 @@ pub fn cmdTargets(
         const triple = try native_target.zigTriple(allocator);
         defer allocator.free(triple);
         try jws.objectField("triple");
-        try jws.emitString(triple);
+        try jws.write(triple);
     }
     {
         try jws.objectField("cpu");
         try jws.beginObject();
         try jws.objectField("arch");
-        try jws.emitString(@tagName(native_target.cpu.arch));
+        try jws.write(@tagName(native_target.cpu.arch));
 
         try jws.objectField("name");
         const cpu = native_target.cpu;
-        try jws.emitString(cpu.model.name);
+        try jws.write(cpu.model.name);
 
         {
             try jws.objectField("features");
@@ -149,8 +141,7 @@ pub fn cmdTargets(
             for (native_target.cpu.arch.allFeaturesList(), 0..) |feature, i_usize| {
                 const index = @as(Target.Cpu.Feature.Set.Index, @intCast(i_usize));
                 if (cpu.features.isEnabled(index)) {
-                    try jws.arrayElem();
-                    try jws.emitString(feature.name);
+                    try jws.write(feature.name);
                 }
             }
             try jws.endArray();
@@ -158,9 +149,9 @@ pub fn cmdTargets(
         try jws.endObject();
     }
     try jws.objectField("os");
-    try jws.emitString(@tagName(native_target.os.tag));
+    try jws.write(@tagName(native_target.os.tag));
     try jws.objectField("abi");
-    try jws.emitString(@tagName(native_target.abi));
+    try jws.write(@tagName(native_target.abi));
     try jws.endObject();
 
     try jws.endObject();
CMakeLists.txt
@@ -205,6 +205,7 @@ set(ZIG_STAGE2_SOURCES
     "${CMAKE_SOURCE_DIR}/lib/std/atomic/queue.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/atomic/stack.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/base64.zig"
+    "${CMAKE_SOURCE_DIR}/lib/std/BitStack.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/buf_map.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/Build.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/Build/Cache.zig"
@@ -260,7 +261,7 @@ set(ZIG_STAGE2_SOURCES
     "${CMAKE_SOURCE_DIR}/lib/std/io/seekable_stream.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/io/writer.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/json.zig"
-    "${CMAKE_SOURCE_DIR}/lib/std/json/write_stream.zig"
+    "${CMAKE_SOURCE_DIR}/lib/std/json/stringify.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/leb128.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/linked_list.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/log.zig"