# Printing nested types with custom formatting

**URL:** <https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742>\
**Category:** Help\
**Tags:** standard-library\
**Created:** [September 28, 2026, 1:16pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742 "2026-09-28T13:16:33Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![matklad](https://ziggit.dev/user_avatar/ziggit.dev/matklad/32/2462_2.png) [@matklad](https://ziggit.dev/u/matklad)\
**Post date:** [September 28, 2026, 1:16pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742/1 "2026-09-28T13:16:34Z")

</div>

I can use `f` to print a type with a custom formatting function. Is there a format specifier that works like `f`, but recursively? Print with `format` if it exists, otherwise fall back to any. Basically, how it used to work in 0.14?

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

const Duration = std.Io.Duration;

const DurationInterval = struct {
    min: Duration,
    max: Duration,
};

pub fn main() void {
    const d: Duration = .fromSeconds(92);

    const di: DurationInterval = .{
        .min = .fromSeconds(92),
        .max = .fromSeconds(100),
    };

    std.debug.print("{f}\n", .{d}); // Prints 1m32s, nice!
    std.debug.print("{}\n", .{di}); // Prints `.{ .min = .{ .nanoseconds =`, urgh.
    std.debug.print("{f}\n", .{di}); // Doesn't compile.
}

```

The use-case here is printf-debugging: I want to be able to dump human-readable representation of anything to `stderr`, without having to explicitly define the said representation for all intermediary data structures.

---

<div class="post-metadata">

**Author:** ![pzittlau](https://ziggit.dev/letter_avatar_proxy/v4/letter/p/ecae2f/32.png) [@pzittlau](https://ziggit.dev/u/pzittlau)\
**Post date:** [September 28, 2026, 2:54pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742/2 "2026-09-28T14:54:45Z")

</div>

I’ve written this one before in `0.15.2`. It still works with `0.16`, but not on master:

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

const Duration = std.Io.Duration;

const DurationInterval = struct {
    min: Duration,
    max: Duration,
};

pub fn main() void {
    const d: Duration = .fromSeconds(92);

    const di: DurationInterval = .{
        .min = .fromSeconds(92),
        .max = .fromSeconds(100),
    };

    std.debug.print("{f}\n", .{d}); // Prints 1m32s, nice!
    std.debug.print("{}\n", .{di}); // Prints `.{ .min = .{ .nanoseconds =`, urgh.
    std.debug.print("{f}\n", .{dump(di)}); // Prints `.{ .min = 1m32s, .max = 1m40s }`
}

/// Wraps any value in an anonymous formatter that recursively dispatches to `{f}` when a type
/// declares a custom `format` method, or falls back to reflection.
pub fn dump(val: anytype) struct {
    data: @TypeOf(val),

    fn formatDump(value: anytype, writer: *std.Io.Writer) std.Io.Writer.Error!void {
        const ValT = @TypeOf(value);
        switch (@typeInfo(ValT)) {
            .@"struct", .@"union", .@"enum" => {
                if (@hasDecl(ValT, "format")) {
                    return writer.print("{f}", .{value});
                }
            },
            else => {},
        }

        switch (@typeInfo(ValT)) {
            .@"struct" => |info| {
                try writer.writeAll(".{ ");
                inline for (info.fields, 0..) |field, i| {
                    if (i > 0) try writer.writeAll(", ");
                    if (!info.is_tuple) {
                        try writer.print(".{s} = ", .{field.name});
                    }
                    try formatDump(@field(value, field.name), writer);
                }
                try writer.writeAll(" }");
            },
            .optional => {
                if (value) |payload| {
                    try formatDump(payload, writer);
                } else {
                    try writer.writeAll("null");
                }
            },
            .error_union => {
                if (value) |payload| {
                    try formatDump(payload, writer);
                } else |err| {
                    try writer.print("{}", .{err});
                }
            },
            .pointer => |info| switch (info.size) {
                .one => try formatDump(value.*, writer),
                .slice => {
                    if (info.child == u8) {
                        try writer.print("{s}", .{value});
                    } else {
                        try writer.writeAll(".{ ");
                        for (value, 0..) |elem, i| {
                            if (i > 0) try writer.writeAll(", ");
                            try formatDump(elem, writer);
                        }
                        try writer.writeAll(" }");
                    }
                },
                else => try writer.print("{}", .{value}),
            },
            .array => |info| {
                if (info.child == u8) {
                    try writer.print("{s}", .{value});
                } else {
                    try writer.writeAll(".{ ");
                    for (value, 0..) |elem, i| {
                        if (i > 0) try writer.writeAll(", ");
                        try formatDump(elem, writer);
                    }
                    try writer.writeAll(" }");
                }
            },
            else => try writer.print("{}", .{value}),
        }
    }

    pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
        return formatDump(self.data, writer);
    }
} {
    return .{ .data = val };
}

```

It doesn’t protect against any recursive types or stuff like that because I didn’t need it. There are likely also other things missing.

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [September 28, 2026, 6:14pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742/3 "2026-09-28T18:14:30Z")

</div>

Why doesn’t it work on master anymore? I have very similar code

---

<div class="post-metadata">

**Author:** ![pzittlau](https://ziggit.dev/letter_avatar_proxy/v4/letter/p/ecae2f/32.png) [@pzittlau](https://ziggit.dev/u/pzittlau)\
**Post date:** [September 28, 2026, 8:17pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742/4 "2026-09-28T20:17:08Z")

</div>

I think it is just, that some things in the structs in the typeinfo tagged union have changed. Likely nothing major.

I’ve just ran it through some relatively new compiler build I had on my machine(less than a week old) and it didn’t compile. I haven’t looked into it because I don’t need it right now.

---

<div class="post-metadata">

**Author:** ![ODON1](https://ziggit.dev/letter_avatar_proxy/v4/letter/o/94ad74/32.png) [@ODON1](https://ziggit.dev/u/ODON1)\
**Post date:** [September 29, 2026, 12:57pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742/5 "2026-09-29T12:57:16Z")

</div>

> [@gwenzek](#):
>
> Why doesn’t it work on master anymore? I have very similar code

Struct doesn’t have fields anymore. Change to field\_names

```zig
        switch (@typeInfo(ValT)) {
            .@"struct" => |info| {
                try writer.writeAll(".{ ");
                inline for (info.field_names, 0..) |field_name, i| {
                    if (i > 0) try writer.writeAll(", ");
                    if (!info.is_tuple) {
                        try writer.print(".{s} = ", .{field_name});
                    }
                    try formatDump(@field(value, field_name), writer);
                }
                try writer.writeAll(" }");
            },

```

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [September 29, 2026, 8:59pm UTC](https://ziggit.dev/t/printing-nested-types-with-custom-formatting/17742/6 "2026-09-29T20:59:54Z")

</div>

ah yes, that’s a trivial change. thanks
