Skip to content

Zig 0.15 cheatsheet

Zig uses 0.x versioning (pre-1.0). What people call “Zig 15” is almost always Zig 0.15.x. As of Oct 11, 2025, the latest stable is 0.15.2; 0.15.1 (Aug 19, 2025) is the one with the big release notes that describe most of the breaking changes you’ll hit when moving to 0.15. (Zig Programming Language)

If you’re upgrading from much older Zig (e.g., 0.12), one practical approach recommended by Zig’s lead is to upgrade one release at a time (0.13 → 0.14 → 0.15), because each step is smaller and you can use each set of release notes. (Ziggit)

This guide focuses on the big 0.15.x breakage areas you named: I/O, Reader/Writer, arrays / ArrayList, and JSON—with “before/after” style migration patterns.


1) The big one: "Writergate" (new I/O model)

What changed conceptually

Zig 0.15 deprecates the old std.io reader/writer interfaces and introduces new non-generic interfaces:

  • std.Io.Reader
  • std.Io.Writer

The key design shift is: the buffer is part of the interface (“buffer above the vtable”), not wrapped via separate BufferedReader/BufferedWriter layers. This is meant to reduce “anytype poisoning”, improve optimizer visibility (especially in Debug), and provide richer stream operations (discard, splat, sendFile, peek, etc.). (Zig Programming Language)

The practical consequences you feel immediately

  • You now provide buffers explicitly in many places.
  • You must flush buffered writers or output may never appear.
  • Lots of stdlib APIs (HTTP, TLS, compression, file APIs) now accept *std.Io.Reader / *std.Io.Writer rather than concrete stream types. (Zig Programming Language)

The new "default" stdout printing pattern (buffer + flush)

Old (pre-0.15-ish)

var stdout = std.io.getStdOut().writer();
try stdout.print("Hello\n", .{});

New (0.15)

const std = @import("std");

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("Hello\n", .{});
    try stdout.flush();
}

This is the recommended migration pattern: buffering + explicit flush. (Zig Programming Language)

"But I just want Hello World"

The language reference still shows a minimal “Hello World” via:

try std.fs.File.stdout().writeAll("Hello, World!\n");

That’s fine for simple output; for formatted/high-frequency output, the buffered writer pattern above is what 0.15 pushes you toward. (Zig Programming Language)


BufferedWriter and CountingWriter are gone (and what replaces them)

std.io.bufferedWriter deleted → you supply the buffer

Old:

var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
const stdout = bw.writer();
try stdout.print("...\n", .{});
try bw.flush();

New:

var stdout_buffer: [4096]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
const stdout = &stdout_writer.interface;

try stdout.print("...\n", .{});
try stdout.flush();

(Zig Programming Language)

CountingWriter deleted → use these instead

  • discard + count: std.Io.Writer.Discarding
  • allocate output: std.Io.Writer.Allocating
  • fixed buffer output: std.Io.Writer.fixed (check .end) (Zig Programming Language)

Adapter: bridging old writers/readers to the new API

If you still have an old-style writer (common while migrating a codebase), there’s an adapter:

fn foo(old_writer: anytype) !void {
    var adapter = old_writer.adaptToNewApi(&.{});
    const w: *std.Io.Writer = &adapter.new_interface;
    try w.print("{s}", .{"example"});
}

This can help you migrate incrementally. (Zig Programming Language)


2) Readers/Writers in 0.15: "how do I actually use them now?"

The "interface pointer" shape (and the consistency trap)

A very common 0.15 stumbling block is: different concrete reader/writer wrappers expose the *std.Io.Reader / *std.Io.Writer differently:

  • Some readers give you an interface() method.
  • Some writers expose an .interface field you take the address of.

This is shown both in official release notes (HTTP server example) and discussed in the community (e.g., the TLS client example). (Zig Programming Language)

Example: net.Stream → TLS client

A minimal “convert Stream.Reader/Writer to Io.Reader/Writer” pattern looks like:

var writer = stream.writer(&write_buf);
var reader = stream.reader(&read_buf);

var tls_client = try std.crypto.tls.Client.init(
    reader.interface(),   // Reader → *std.Io.Reader
    &writer.interface,    // Writer → *std.Io.Writer
    .{},
);

Two important gotchas called out in practice:

  1. Reader/writer must have a stable address (don’t take pointers to temporaries).
  2. The buffer sizes may need to meet minimums (TLS documents a minimum like std.crypto.tls.max_ciphertext_record_len). (openmymind.net)

Reading: line-based input changed (and the error model is more explicit)

Release notes show a new pattern for delimiter-based reading that surfaces actionable errors such as:

Example (from the new API style):

while (reader.takeDelimiterExclusive('\n')) |line| {
    // use line
} else |err| switch (err) {
    error.EndOfStream,
    error.StreamTooLong,
    error.ReadFailed,
    => |e| return e,
}

(Zig Programming Language)

You’ll also see simpler “read line” patterns in updated community guides using methods like takeDelimiter. (Zig Guide)


Reading a file into memory using the new reader

A concrete example of the new file.reader(&buffer) style:

const file = try std.fs.cwd().createFile("junk_file2.txt", .{ .read = true });
defer file.close();

try file.writeAll("Hello File!");
try file.seekTo(0);

var file_buffer: [1024]u8 = undefined;
var file_reader = file.reader(&file_buffer);

const contents = try file_reader.interface.readAlloc(std.testing.allocator, 1024);
defer std.testing.allocator.free(contents);

This highlights a few 0.15 realities:

  • you supply a buffer when creating the reader,
  • you call through the reader interface (file_reader.interface...). (Zig Guide)

3) Formatting + print breakage: {f} is now required for format methods

The new rule

If a value has a format method, plain {} can become ambiguous. Zig 0.15 requires you to explicitly say:

Example from the release notes:

std.debug.print("{f}", .{std.zig.fmtId("example")});

(Zig Programming Language)

Why it matters for JSON

std.json.fmt(...) produces a value intended to be formatted via a format method—so you typically print it with {f} (more in the JSON section). (Zig Guide)


  • Formatted alignment is now ASCII/bytes-only, not Unicode-aware. If you were depending on Unicode column alignment, you’ll need your own Unicode-width handling. (Zig Programming Language)
  • std.fmt.format is deprecated in favor of std.Io.Writer.print. (Zig Programming Language)

4) Arrays in 0.15: ArrayList flipped (unmanaged is now the default)

This is the other big “every codebase feels it” change.

What changed

  • Old std.ArrayList (managed, stored an allocator) moved to:

  • std.array_list.Managed

  • The default std.ArrayList is now the unmanaged-style API (allocator passed to methods). The managed variants are expected to be removed eventually. (Zig Programming Language)

A community summary puts it bluntly:

what used to be ArrayListUnmanaged is now ArrayList … old ArrayList is now std.array_list.Managed. (Ziggit)

Migration patterns

Pattern A: building a growable byte buffer (string builder)

0.15-style (allocator passed explicitly):

pub fn build_query(allocator: std.mem.Allocator, params: []Param) ![]u8 {
    var response = try std.ArrayList(u8).initCapacity(allocator, 64);

    for (params) |param| {
        if (response.items.len > 0) try response.append(allocator, '&');
        try response.appendSlice(allocator, param.name);
        try response.append(allocator, '=');
        try response.appendSlice(allocator, param.value);
    }

    return response.toOwnedSlice(allocator);
}

This is exactly the “new normal”: allocator is not stored; you pass it in. (Ziggit)

Pattern B: "I just want an empty list and append"

var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);

try list.append(allocator, 'A');
try list.appendSlice(allocator, "BC");

The .empty + deinit(allocator) style is used in updated 0.15 guides. (Zig Guide)

Pattern C: formatted append directly into an ArrayList

ArrayList(u8) can act like a string builder with print:

var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);

try list.print(allocator, "Hello {s}!", .{"World"});

(Zig Guide)


BoundedArray removed: what to use instead

std.BoundedArray is removed. The release notes recommend three broad migration choices:

  1. If the “bound” is arbitrary / guessy → don’t guess; accept a buffer slice from the caller or use heap allocation.
  2. If it’s “type safety around a stack buffer” → use ArrayList (unmanaged) backed by a fixed buffer.
  3. If it’s a rare fixed-capacity ordered set → hand-roll it. (Zig Programming Language)

The notes show replacing BoundedArray with initBuffer(&buffer) + bounded append operations. (Zig Programming Language)


0.15 deletes several ring-buffer implementations (std.fifo.LinearFifo, std.RingBuffer, etc.), explicitly pointing out that the new std.Io.Reader / std.Io.Writer are themselves ring buffers and cover many of the prior use cases. std.fifo is deleted. (Zig Programming Language)

If your code used std.fifo/queues, expect to either:

  • switch to a different std container (if available),
  • adopt a third-party deque/queue,
  • or implement a small specialized structure.

5) JSON in Zig 0.15: parsing is familiar; writing changed because I/O changed

Parsing JSON: parseFromSlice still looks like you remember

Example:

const Place = struct { lat: f32, long: f32 };

const parsed = try std.json.parseFromSlice(
    Place,
    allocator,
    \\{ "lat": 40.684540, "long": -74.401422 }
,
    .{},
);
defer parsed.deinit();

const place = parsed.value;

Key points:

  • you pass an allocator,
  • you deinit() the parsed result to free allocations. (Zig Guide)

Writing / stringifying JSON: two good 0.15-native approaches

Approach A: std.json.fmt(...) + print with {f}

This is very ergonomic when you already have a Writer and want formatting control:

try writer.print("{f}", .{std.json.fmt(value, .{})});

A full “stringify into an allocated string” example uses an allocating writer and the {f} format specifier:

var out: std.Io.Writer.Allocating = .init(allocator);
defer out.deinit();

try out.writer.print("{f}", .{std.json.fmt(x, .{})});
const bytes = out.written();

This pattern is shown in up-to-date 0.15 guides, and {f} is required due to the 0.15 formatting rule change. (Zig Guide)

Approach B: std.json.Stringify.value(...) writing directly to a *std.Io.Writer

This is great for fixed buffers and for streaming to files/sockets.

Fixed buffer example:

var buffer: [256]u8 = undefined;
var w = std.Io.Writer.fixed(&buffer);

try std.json.Stringify.value(.{
    .a_number = @as(u32, 10),
    .a_str = "hello",
}, .{}, &w);

const json_bytes = buffer[0..w.end];

That exact shape is used in modern 0.15 examples. (Renato Athaydes)


Writing JSON to a file in 0.15 (putting it all together)

A practical pattern:

  1. Create a buffered file writer
  2. Write JSON using either method
  3. flush()
const std = @import("std");

pub fn writeJsonToStdout(value: anytype) !void {
    var buf: [4096]u8 = undefined;
    var fw = std.fs.File.stdout().writer(&buf);
    const out = &fw.interface;

    try out.print("{f}\n", .{std.json.fmt(value, .{})});
    try out.flush();
}

This combines:


6) High-signal "rename/deletion" cheat sheet for these areas

From the 0.15 release notes’ “Deletions and Deprecations” section (selected items that commonly break builds): (Zig Programming Language)

I/O / Reader / Writer

  • std.io.GenericReaderstd.Io.Reader
  • std.io.GenericWriterstd.Io.Writer
  • std.io.AnyReaderstd.Io.Reader
  • std.io.AnyWriterstd.Io.Writer
  • std.fs.File.readerstd.fs.File.deprecatedReader
  • std.fs.File.writerstd.fs.File.deprecatedWriter
  • deleted: std.io.SeekableStream → use *std.fs.File.Reader, *std.fs.File.Writer, or an in-memory concrete type like ArrayList (depending on what you’re actually doing). (Zig Programming Language)
  • deleted: std.Io.BufferedReader
  • deleted: std.io.bufferedWriter (BufferedWriter) → supply a buffer to the writer directly. (Zig Programming Language)

Arrays / ArrayList

  • std.ArrayList (managed) → std.array_list.Managed (planned for eventual removal)
  • default std.ArrayList is now unmanaged-style (allocator passed to methods). (Zig Programming Language)
  • removed: std.BoundedArray → use caller-provided buffers, allocation, or ArrayList backed by a stack buffer. (Zig Programming Language)

JSON

  • Parsing: std.json.parseFromSlice remains the go-to for “parse JSON bytes into a type.” (Zig Guide)
  • Writing: prefer std.json.fmt + {f}, or std.json.Stringify.value to a *std.Io.Writer. (Zig Guide)

7) Common 0.15 migration errors and what they mean

"Nothing prints"

You forgot to flush() your buffered writer. The release notes explicitly warn about this, and it’s the most common surprise. (Zig Programming Language)

"ambiguous format string; specify {f} … or {any} …"

You’re printing something that provides a format method (e.g. std.zig.fmtId, std.json.fmt, and many more). Update {} to {f} (or {any} if you explicitly want the raw debug-ish representation). (Zig Programming Language)

"expected type *std.Io.Writer, found …"

A stdlib API now wants the interface pointer, not your concrete writer type.

  • For many writers you pass &some_writer.interface
  • For some readers you pass some_reader.interface()

Also ensure the underlying objects live long enough (stable address). (Zig Programming Language)

ArrayList: "method requires allocator parameter"

That’s expected: in 0.15 the default ArrayList no longer stores the allocator. Update calls like:

  • list.append(x)list.append(allocator, x)
  • list.deinit()list.deinit(allocator)
  • list.toOwnedSlice()list.toOwnedSlice(allocator) (Ziggit)

8) A practical upgrade checklist (I/O + arrays + JSON)

  1. Pick Zig 0.15.2 as your target compiler. (Zig Programming Language)
  2. If upgrading from older Zig, do sequential upgrades (0.13 → 0.14 → 0.15) and read each release note set. (Ziggit)
  3. Replace old stdout/stderr patterns:

  4. std.io.getStdOut().writer()std.fs.File.stdout().writer(&buf) + flush() (Zig Programming Language)

  5. Remove bufferedWriter/CountingWriter usage; switch to explicit buffers and the new helper writers. (Zig Programming Language)
  6. Fix formatting compilation errors:

  7. {}{f} where needed

  8. use std.Io.Writer.print instead of old std.fmt.format-centric patterns (Zig Programming Language)
  9. Convert ArrayList usage:

  10. assume allocator is now an argument to methods

  11. use .empty, initCapacity, deinit(allocator) patterns (Zig Programming Language)
  12. JSON:

  13. parsing likely unchanged

  14. writing: use std.json.fmt + {f} or std.json.Stringify.value to a *std.Io.Writer (Zig Guide)

If you want, paste one or two representative snippets from your pre-0.15 code (one I/O example + one ArrayList/JSON example) and I’ll rewrite them into idiomatic 0.15.2 style using the new interfaces—no “mystery anytype”, explicit allocator passing, and correct flush behavior.


Below is a “how to write idiomatic Zig” guide aimed at Zig 0.15.x (what you called “Zig 15”). I’m going to assume Zig 0.15.2 as the reference point (released 2025‑10‑11). (Zig Programming Language)


Idiomatic Zig 0.15: style, patterns, and cheatsheets

The mindset that produces idiomatic Zig

Zig’s own “Zen” is a good north star. It’s not “rules”, but it explains why idioms look the way they do. (Zig Programming Language)

Two other “philosophy facts” drive idioms a lot:

  • Zig tries hard to avoid hidden control flow and hidden allocations—you can usually trust what you see. (Zig Programming Language)
  • Zig expects you to handle allocation failure and to pass allocators into code that needs them. (Zig Programming Language)

"Style" in Zig: mostly just zig fmt

The golden rule

Run zig fmt and don’t fight it. The official style guide explicitly says zig fmt will apply the recommendations and that a style guide is only needed for cases where zig fmt doesn’t format something. (Zig Programming Language)

Naming conventions (official)

The language reference’s style guide lays out the conventions most people treat as canonical: (Zig Programming Language)

Doc comments (official)

  • /// documents the next declaration.
  • //! documents the containing thing (often a file/module). (Zig Programming Language)

The style guide also recommends a useful convention in API docs:

  • Use “Assume” for preconditions that are illegal behavior if violated.
  • Use “Assert” for preconditions that are checked and produce safety-checked failure. (Zig Programming Language)

Bonus: generating docs

Doc comments can be emitted as HTML using zig test -femit-docs …. (Zig Programming Language)


Idiomatic defaults: const, explicit lifetimes, explicit ownership

Prefer const by default

Idiomatic Zig code is aggressively immutable until it must be mutable:

const std = @import("std");

pub fn main() !void {
    const greeting = "hello";
    var counter: usize = 0;

    // counter changes, greeting doesn’t.
    counter += 1;
    _ = greeting;
}

This aligns with “communicate intent precisely”: mutability stands out.

Ownership & lifetime are part of the API surface

The language reference is blunt: it’s the programmer’s responsibility to ensure pointers aren’t used after the memory is gone, and docs should explain who “owns” returned pointers. (Zig Programming Language)

Idiomatic library functions that return allocated memory typically follow this contract:

  • Function takes allocator: std.mem.Allocator
  • Return value is ![]u8 / ![]T
  • Doc says: caller owns returned memory → caller frees it with the same allocator

Example pattern:

/// Reads an entire file into memory.
/// Caller owns the returned buffer and must free it with `allocator.free`.
fn read_file_alloc(allocator: std.mem.Allocator, path: []const u8, max: usize) ![]u8 {
    const std = @import("std");

    const file = try std.fs.cwd().openFile(path, .{ .read = true });
    defer file.close();

    var buf: [4096]u8 = undefined;
    var r = file.reader(&buf);

    // readAlloc allocates up to `max`, else error (e.g. StreamTooLong).
    const data = try r.interface.readAlloc(allocator, max);
    return data;
}

That file.reader(&buf) + r.interface.readAlloc(...) pattern matches current 0.15-era usage. (Zig Guide)


Strings, bytes, and "arrays vs slices" (the idiomatic mental model)

Strings are bytes

In Zig, “strings” are usually just []const u8 (a byte slice you treat as UTF‑8).

Arrays vs slices (practical meaning)

  • [N]T is an array value with length known at compile time.
  • []T is a slice: pointer + length (a view into something else).
  • []const u8 is the most common “string view”.

String literals are not mutable slices

The language reference shows this sharp edge clearly: string literals have an array pointer type, and you can’t assign them to []u8 (mutable slice).

If you want a mutable buffer, allocate or use an array:

var buf: [13]u8 = "hello, world!".*; // make a mutable copy
const slice: []u8 = buf[0..];

Errors & optionals: idiomatic control flow tools

Errors are values and can't be silently ignored

Zig will complain if you discard an error union without handling it, and it tells you to use try, catch, or if. (Zig Programming Language)

Idiomatic patterns

1) Propagate with try

const file = try std.fs.cwd().openFile("x.txt", .{});
defer file.close();

2) Handle or map with catch

const file = std.fs.cwd().openFile("x.txt", .{}) catch |err| switch (err) {
    error.FileNotFound => return, // treat as “no-op”
    else => return err,
};
defer file.close();

3) Cleanup on errors with errdefer This is the “idiomatic RAII substitute”: allocate in steps, errdefer cleanup each step.

var list: std.ArrayList(u8) = .empty;
errdefer list.deinit(allocator);

try list.appendSlice(allocator, "hello");
// if later code errors, list gets deinit’d automatically

4) Use ?T for “maybe present”, not error

  • ?T means “this is allowed to be missing”
  • error!T means “this operation can fail”

Heap allocation failure is a first-class error

Zig’s docs explicitly recommend treating error.OutOfMemory as the representation of heap allocation failure, rather than unconditionally crashing. (Zig Programming Language)

That’s why idiomatic Zig APIs:

  • take allocators explicitly
  • return error.OutOfMemory when they allocate

Resource management: defer and "make cleanup obvious"

Idiomatic Zig tries to make resource cleanup visually checkable:

const file = try std.fs.cwd().openFile("data.bin", .{});
defer file.close(); // always runs, even on error

// ...

Use errdefer when the cleanup is only correct for the error path (e.g., before “ownership” has been transferred).


Containers in Zig 0.15: ArrayList is unmanaged by default

The big idiom shift

In Zig 0.15, the unmanaged variant is the default:

  • std.ArrayListstd.array_list.Managed (old “allocator-storing” flavor)
  • the default std.ArrayList now follows the “pass allocator to methods” style The release notes explain the rationale and warn the managed names will eventually be removed. (Zig Programming Language)

Idiomatic ArrayList usage (0.15)

Use .empty, pass an allocator to operations, and deinit(allocator):

const std = @import("std");

test "arraylist basics" {
    const allocator = std.testing.allocator;

    var list: std.ArrayList(u8) = .empty;
    defer list.deinit(allocator);

    try list.appendSlice(allocator, "Hello");
    try list.appendSlice(allocator, " World!");

    try std.testing.expectEqualStrings("Hello World!", list.items);
}

That matches common 0.15-era examples. (Zig Guide)

Stack-buffer backed "array list"

0.15 also pushes a pattern: if you want a fixed maximum but stack storage, use ArrayListUnmanaged.initBuffer(&buffer) and bounded appends. (Zig Programming Language)


I/O in Zig 0.15: the idiomatic Reader/Writer style

Zig 0.15’s I/O story heavily influences “idiomatic Zig”, because it nudges you to write APIs that accept readers/writers rather than concrete streams or generic types.

The core idiom

  • Create a writer/reader with an explicit buffer
  • Pass around *std.Io.Writer / *std.Io.Reader (the interface)
  • Remember to flush() when you need output visible

The release notes are explicit: “Please use buffering! And don’t forget to flush!” and show the new stdout pattern. (Zig Programming Language)

Example (stdout):

const std = @import("std");

pub fn main() !void {
    var buf: [1024]u8 = undefined;
    var w = std.fs.File.stdout().writer(&buf);
    const stdout = &w.interface;

    try stdout.print("Hello, {s}!\n", .{"world"});
    try stdout.flush();
}

Write functions that accept a writer (idiomatic library design)

fn greet(writer: *std.Io.Writer, name: []const u8) !void {
    try writer.print("Hello, {s}!\n", .{name});
}

This is idiomatic because:

  • it avoids hidden allocations (callers choose buffering / destination)
  • it composes with files, sockets, memory writers, etc.

std.debug.print is for "debug output; ignore errors"

The language reference notes that std.debug.print is appropriate for stderr where errors are irrelevant, and it’s simpler than building your own writer. (Zig Programming Language)


Formatting in Zig 0.15: {f}, new format signature, and "print everywhere"

Prefer writer.print(...) over std.fmt.format

0.15 deprecates/redirects older formatting patterns toward writers:

{f} is how you call a type's format method now

Zig 0.15.1 changed custom formatting:

  • Old format took a format string + options + anytype writer
  • New format is:
pub fn format(this: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void

(Zig Programming Language)

And it’s invoked with {f} rather than {}. (Zig Programming Language)

std.fmt.allocPrint and friends are still idiomatic when you need a string

If you truly want a string in memory, std.fmt.allocPrint is a straightforward idiom. (Zig Guide)

const s = try std.fmt.allocPrint(allocator, "{d} + {d}", .{ 1, 2 });
defer allocator.free(s);

Formatting specifier cheat sheet (common ones)

A few that show up constantly (examples are from Zig 0.15.2 guides): (Zig Guide)

  • {s} string / byte slice
  • {d} decimal (ints & floats)
  • {x} / {X} hex (lower/upper)
  • {b} binary, {o} octal
  • {c} ASCII character for a byte
  • {*} pointer address formatting (Zig Guide)
  • {e} scientific notation (Zig Guide)
  • {t} shorthand for @tagName() / @errorName() (Zig Programming Language)
  • {B} / {Bi} size formatting variants (Zig Guide)
  • {f} call .format(writer) on a value (Zig Programming Language)

JSON in Zig 0.15: idiomatic parsing & printing

Parse into a typed struct (idiomatic)

The “typed parse” pattern is:

  • std.json.parseFromSlice(T, allocator, input, options)
  • defer parsed.deinit()
  • use parsed.value

Example: (Zig Guide)

const Place = struct { lat: f32, long: f32 };

const parsed = try std.json.parseFromSlice(
    Place,
    allocator,
    input_bytes,
    .{},
);
defer parsed.deinit();

const place = parsed.value;

Stringify / format JSON (idiomatic)

A convenient idiom in 0.15 is: create an allocating writer, then print("{f}", .{std.json.fmt(value, .{})}). (Zig Guide)

var out: std.io.Writer.Allocating = .init(allocator);
defer out.deinit();

try out.writer.print("{f}", .{std.json.fmt(value, .{})});
const json_bytes = out.written();

(Notice the {f}: it matches the 0.15 formatting model.) (Zig Guide)

Also note: JSON parsing needs an allocator for strings/arrays/maps inside JSON data. (Zig Guide)


Build system (0.15 idioms): root_module + createModule

A minimal, idiomatic build.zig in 0.15 looks like this (from Zig’s build system docs): (Zig Programming Language)

const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = b.createModule(.{
            .root_source_file = b.path("hello.zig"),
            .target = b.graph.host,
        }),
    });

    b.installArtifact(exe);
}

If you’re coming from older versions: 0.15 removed deprecated root module fields, so this root_module = b.createModule(...) style is the “new normal”. (Zig Programming Language)


Comptime & generics: idiomatic guidance (practical, not dogmatic)

Zig idioms around generics tend to favor:

  • Use comptime and @TypeOf when it improves clarity, not just because you can.
  • Prefer a *std.Io.Writer / *std.Io.Reader argument (interface) over anytype writer/reader in public APIs if you want to avoid code bloat and keep call sites stable—0.15’s I/O changes reinforce this. (Zig Programming Language)
  • When supporting multiple Zig versions, prefer feature detection (@hasDecl, @hasField) over version checks. (Zig Programming Language)

Cheatsheets

1) Declarations & basics

const std = @import("std");

// immutable binding
const x: i32 = 123;

// mutable binding
var y: usize = 0;

// function
fn add(a: i32, b: i32) i32 {
    return a + b;
}

// error-returning main (common)
pub fn main() !void {}

// test
test "something" {
    try std.testing.expect(true);
}

2) Types: arrays, slices, pointers

  • [N]T → array value (stack-allocated when local)
  • []T → slice (ptr + len)
  • []const u8 → most common “string”
  • *T → single-item pointer (non-null)
  • ?*T → optional pointer
  • [*]T → many-item pointer (unknown length)
  • [*:0]const u8 → sentinel-terminated pointer (C string)
  • [:0]u8 → sentinel-terminated slice

String literal reminder: not a mutable []u8 by default.

3) Optionals (?T)

const maybe: ?u32 = 10;

const v1: u32 = maybe orelse 0;

if (maybe) |v| {
    // v: u32
} else {
    // was null
}

4) Errors (error!T) and propagation

fn mightFail() !u32 {
    return 123;
}

pub fn main() !void {
    const v = try mightFail();
    _ = v;
}

Handle an error:

const v = mightFail() catch |err| switch (err) {
    error.OutOfMemory => 0,
    else => return err,
};

5) Resource cleanup (defer / errdefer)

const file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();

// allocate
var list: std.ArrayList(u8) = .empty;
errdefer list.deinit(allocator);

try list.appendSlice(allocator, "hi");
defer list.deinit(allocator); // once ownership is “committed”

6) ArrayList (0.15 style)

Heap-backed:

var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);

try list.append(allocator, 'A');
try list.appendSlice(allocator, "BC");

(That .empty + deinit(allocator) style is standard 0.15-era usage.) (Zig Guide)

Stack-backed buffer:

var buf: [64]u8 = undefined;
var list = std.ArrayListUnmanaged(u8).initBuffer(&buf);
// bounded appends exist in 0.15 era

(Zig Programming Language)

7) I/O (0.15 stdout writer pattern)

var buf: [1024]u8 = undefined;
var w = std.fs.File.stdout().writer(&buf);
const out = &w.interface;

try out.print("Hello {s}\n", .{"world"});
try out.flush();

(Zig Programming Language)

8) "Write to memory" patterns

  • Use std.ArrayList(u8) and list.print(allocator, ...) (Zig Guide)
  • Or use an allocating writer for formatting / JSON output (Zig Guide)

Example with ArrayList:

var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);

try list.print(allocator, "Hello {s}!", .{"World"});

(Zig Guide)

9) Formatting quick ref

10) JSON quick ref

Parse:

const parsed = try std.json.parseFromSlice(T, allocator, bytes, .{});
defer parsed.deinit();
const value = parsed.value;

(Zig Guide)

Stringify / print:

try writer.print("{f}", .{std.json.fmt(value, .{})});

(Zig Guide)

11) Build.zig "hello world"

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = b.createModule(.{
            .root_source_file = b.path("hello.zig"),
            .target = b.graph.host,
        }),
    });
    b.installArtifact(exe);
}

(Zig Programming Language)


If you want, I can also provide an “idiomatic Zig 0.15 project skeleton” (file layout + main.zig template + build.zig + common helper modules), but the core patterns above are the ones that make Zig code feel idiomatic in 0.15—especially the allocator discipline, the ownership documentation habit, and the new Reader/Writer + {f} formatting ecosystem.