# Custom Formatting

### Custom Formatting

Zig has a formatting system built into the standard library. You have already used it many times through `std.debug.print`.

```zig
const std = @import("std");

pub fn main() void {
    std.debug.print("name = {s}, age = {}\n", .{ "Ada", 36 });
}
```

This prints:

```text
name = Ada, age = 36
```

The first argument is the format string:

```zig
"name = {s}, age = {}\n"
```

The second argument is a tuple of values:

```zig
.{ "Ada", 36 }
```

The braces inside the format string tell Zig where to put each value.

```zig
{s}
```

means “format this value as a string.”

```zig
{}
```

means “format this value using its default format.”

Custom formatting means this: you define how your own type should appear when it is printed.

#### Why Custom Formatting Exists

Suppose we have a `Point` type:

```zig
const Point = struct {
    x: i32,
    y: i32,
};
```

A point has two numbers. We might want to print it like this:

```text
Point(10, 20)
```

Or like this:

```text
{x = 10, y = 20}
```

Or like this:

```text
10,20
```

Different programs need different output styles. Debug output, logs, terminal output, config files, and generated code may all want different text.

Without custom formatting, every call site must manually decide how to print the value:

```zig
std.debug.print("Point({}, {})\n", .{ p.x, p.y });
```

That works, but it spreads formatting logic across the program. If you later change how points should be printed, you must find every print call and update it.

Custom formatting lets the type own its display logic.

#### The Basic Idea

In Zig, a type can provide a `format` method.

Here is a simple example:

```zig
const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,

    pub fn format(
        self: Point,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("Point({}, {})", .{ self.x, self.y });
    }
};

pub fn main() void {
    const p = Point{ .x = 10, .y = 20 };

    std.debug.print("{}\n", .{p});
}
```

This prints:

```text
Point(10, 20)
```

The important part is this method:

```zig
pub fn format(
    self: Point,
    writer: *std.Io.Writer,
) !void {
    try writer.print("Point({}, {})", .{ self.x, self.y });
}
```

When Zig sees this:

```zig
std.debug.print("{}\n", .{p});
```

it notices that `Point` has a `format` method, then calls that method to write the value.

#### Formatting Writes to a Writer

A formatter does not return a string.

It writes bytes into a writer.

That is an important Zig idea. Building strings often requires allocation. Writing directly to a writer avoids unnecessary memory allocation.

This is efficient:

```zig
try writer.print("Point({}, {})", .{ self.x, self.y });
```

This would be less direct:

```zig
// Build a string first, then print it later.
```

Zig prefers direct output when possible.

A writer can represent many destinations:

```text
terminal
file
memory buffer
network stream
test buffer
```

The same custom formatter can work with all of them.

#### A More Complete Example

Let us define a `User` type.

```zig
const std = @import("std");

const User = struct {
    id: u64,
    name: []const u8,
    active: bool,

    pub fn format(
        self: User,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("User(id={}, name=\"{s}\", active={})", .{
            self.id,
            self.name,
            self.active,
        });
    }
};

pub fn main() void {
    const user = User{
        .id = 42,
        .name = "Ada",
        .active = true,
    };

    std.debug.print("{}\n", .{user});
}
```

Output:

```text
User(id=42, name="Ada", active=true)
```

Now every place that prints a `User` with `{}` gets the same representation.

```zig
std.debug.print("created: {}\n", .{user});
std.debug.print("loaded: {}\n", .{user});
std.debug.print("deleted: {}\n", .{user});
```

The formatting rule lives in one place: the `User` type.

#### Formatting Should Not Hide Expensive Work

A formatter should usually be simple.

Good formatter:

```zig
try writer.print("User(id={}, name=\"{s}\")", .{
    self.id,
    self.name,
});
```

Risky formatter:

```zig
// Avoid doing database access, network calls, or large allocations here.
```

Formatting is often used in logs, errors, tests, and debugging. If printing a value secretly performs expensive work, the program becomes harder to reason about.

A good rule: formatting should describe a value, not compute the value from scratch.

#### Debug Formatting vs Display Formatting

Sometimes you want different output for different purposes.

For humans:

```text
Ada
```

For debugging:

```text
User(id=42, name="Ada", active=true)
```

For a file format:

```text
42,Ada,true
```

Zig’s formatting system can support different format specifiers. Conceptually, a formatter can inspect the requested formatting style and choose different output.

A simplified design looks like this:

```zig
const User = struct {
    id: u64,
    name: []const u8,
    active: bool,

    pub fn format(
        self: User,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("User(id={}, name=\"{s}\", active={})", .{
            self.id,
            self.name,
            self.active,
        });
    }
};
```

For many beginner and intermediate programs, one default custom format is enough. Start there. Add multiple styles only when you have a real need.

#### Formatting Nested Values

Custom formatting becomes more useful when types contain other custom types.

```zig
const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,

    pub fn format(
        self: Point,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("Point({}, {})", .{ self.x, self.y });
    }
};

const Rectangle = struct {
    top_left: Point,
    bottom_right: Point,

    pub fn format(
        self: Rectangle,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("Rectangle({}, {})", .{
            self.top_left,
            self.bottom_right,
        });
    }
};

pub fn main() void {
    const rect = Rectangle{
        .top_left = Point{ .x = 0, .y = 0 },
        .bottom_right = Point{ .x = 100, .y = 50 },
    };

    std.debug.print("{}\n", .{rect});
}
```

Output:

```text
Rectangle(Point(0, 0), Point(100, 50))
```

Notice this part:

```zig
try writer.print("Rectangle({}, {})", .{
    self.top_left,
    self.bottom_right,
});
```

`Rectangle` does not manually print `Point.x` and `Point.y`. It lets `Point` format itself.

This is the main advantage of custom formatting. Each type knows how to describe itself.

#### Formatting Collections

Now consider a small list-like type.

```zig
const std = @import("std");

const Scores = struct {
    values: []const u32,

    pub fn format(
        self: Scores,
        writer: *std.Io.Writer,
    ) !void {
        try writer.writeAll("[");

        for (self.values, 0..) |value, i| {
            if (i != 0) {
                try writer.writeAll(", ");
            }

            try writer.print("{}", .{value});
        }

        try writer.writeAll("]");
    }
};

pub fn main() void {
    const values = [_]u32{ 90, 75, 88 };
    const scores = Scores{ .values = values[0..] };

    std.debug.print("scores = {}\n", .{scores});
}
```

Output:

```text
scores = [90, 75, 88]
```

This formatter writes the opening bracket, then each value, then the closing bracket.

The loop uses an index:

```zig
for (self.values, 0..) |value, i| {
```

The index lets us avoid printing a comma before the first item.

```zig
if (i != 0) {
    try writer.writeAll(", ");
}
```

This is a common pattern for formatting lists.

#### Formatting Must Handle Errors

A formatter returns an error union:

```zig
!void
```

Writing can fail. A file can fail. A stream can fail. A buffer can run out of space.

That is why formatting code uses `try`:

```zig
try writer.writeAll("[");
try writer.print("{}", .{value});
try writer.writeAll("]");
```

Do not ignore these errors. The caller decides what to do with them.

This is normal Zig style: if an operation can fail, the type says so.

#### Avoid Allocating Inside Formatters

Try not to allocate memory inside a formatter.

Prefer this:

```zig
try writer.print("Point({}, {})", .{ self.x, self.y });
```

Avoid this kind of design unless you really need it:

```zig
// Allocate a temporary string.
// Print the temporary string.
// Free the temporary string.
```

A formatter may be called in many places. It may be used during error reporting. It may be used while debugging allocation problems. If the formatter itself allocates, debugging becomes harder.

Direct writing is simpler and usually faster.

#### Keep Formatting Stable

Once a type has a formatter, other code may start depending on the output.

This is especially true for:

```text
logs
tests
snapshots
generated files
command-line output
```

Before changing custom formatting, ask whether the text is part of a stable interface.

For debugging, changing the format is usually fine.

For command-line tools, changing output can break scripts.

For generated files, changing output can create noisy diffs.

For logs, changing output can break parsers.

A formatter is just code, but its output may become part of your program’s contract.

#### Custom Formatting for Tests

Custom formatting is useful in tests because it makes failed output easier to read.

Suppose a test prints this:

```text
expected Point(10, 20), got Point(10, 21)
```

That is much better than a vague message.

Example:

```zig
const std = @import("std");
const testing = std.testing;

const Point = struct {
    x: i32,
    y: i32,

    pub fn format(
        self: Point,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("Point({}, {})", .{ self.x, self.y });
    }
};

test "points are equal" {
    const expected = Point{ .x = 10, .y = 20 };
    const actual = Point{ .x = 10, .y = 21 };

    if (expected.x != actual.x or expected.y != actual.y) {
        std.debug.print("expected {}, got {}\n", .{ expected, actual });
        return error.TestExpectedEqual;
    }
}
```

The custom formatter makes the test failure clearer.

#### Custom Formatting and API Design

A public type should have a public formatting story.

For a small internal type, you can choose any output that helps you debug.

For a library type, be more careful. Users may print your values in their logs or CLI tools. The output should be clear, predictable, and boring.

Good public formatting:

```text
Duration(seconds=3, nanos=500000000)
```

Less useful formatting:

```text
duration thing!!!
```

Cute output is fun once. Clear output remains useful for years.

Prefer names and structure that help the reader understand the value.

#### Common Mistakes

A common mistake is trying to return a formatted string from the formatter. In Zig, the formatter writes to a writer.

Another common mistake is forgetting `try`. Writer operations can fail, so the formatter must propagate errors.

A third mistake is putting too much logic in the formatter. The formatter should present data. It should not modify the object, fetch remote data, or perform unrelated work.

A fourth mistake is making the output too clever. Use clear text. Future readers, logs, and tests all benefit from plain structure.

#### A Practical Pattern

For most custom types, start with this pattern:

```zig
const MyType = struct {
    field_a: u32,
    field_b: []const u8,

    pub fn format(
        self: MyType,
        writer: *std.Io.Writer,
    ) !void {
        try writer.print("MyType(field_a={}, field_b=\"{s}\")", .{
            self.field_a,
            self.field_b,
        });
    }
};
```

This gives you:

```text
MyType(field_a=123, field_b="hello")
```

It is readable. It is stable. It includes field names. It works well in logs and tests.

#### The Main Idea

Custom formatting lets your types control how they are printed.

Instead of scattering print logic across the program, you attach that logic to the type itself.

A good formatter is simple, direct, and predictable. It writes to a writer, propagates errors, avoids unnecessary allocation, and produces output that helps humans understand the value.

