Reflection means a program can inspect information about types while the program is being compiled or running.
Reflection means a program can inspect information about types while the program is being compiled or running.
Some languages provide runtime reflection. A program can ask questions like:
What fields does this struct contain?
What is the name of this type?
How many arguments does this function take?In many languages, this information exists at runtime inside the final executable.
Zig takes a different approach.
Zig reflection is mostly compile-time reflection.
That means the compiler can inspect types while compiling the program. This is faster, safer, and simpler than large runtime reflection systems.
The center of Zig reflection is:
@typeInfoThis builtin lets you inspect the structure of a type.
Why Reflection Matters
Reflection sounds advanced, but it solves practical problems.
Suppose you want to:
serialize structs to JSON
build command-line parsers
generate debug output
write generic containers
automatically validate data
build ORMs or config systemsWithout reflection, you often repeat the same information manually.
Example:
const User = struct {
id: u64,
name: []const u8,
active: bool,
};Then somewhere else:
// Serialize "id"
// Serialize "name"
// Serialize "active"And somewhere else again:
// Validate "id"
// Validate "name"
// Validate "active"Reflection lets the compiler inspect the struct fields automatically.
Reflection in Zig Happens at Compile Time
This is one of the most important ideas in Zig.
When Zig reflects on a type, it usually does so during compilation, not while the program is running.
That means:
less runtime overhead
smaller binaries
more optimization opportunities
fewer hidden runtime systemsReflection in Zig is not a giant object system with runtime metadata everywhere.
Instead, Zig treats types as compile-time values.
The Simplest Reflection Example
Let us inspect a type.
const std = @import("std");
const User = struct {
id: u64,
name: []const u8,
active: bool,
};
pub fn main() void {
const info = @typeInfo(User);
std.debug.print("{}\n", .{info});
}@typeInfo(User)asks the compiler:
Tell me about the structure of User.The result is a tagged union describing the type.
For a struct, the result contains information like:
field names
field types
field count
layout informationUnderstanding @typeInfo
@typeInfo returns a value of type:
std.builtin.TypeThis is a large tagged union describing every kind of Zig type.
Examples:
Struct
Enum
Union
Array
Pointer
Optional
Fn
Int
Float
VectorA type is itself data that the compiler can inspect.
This is a major Zig idea.
Inspecting Struct Fields
Now let us inspect the fields inside a struct.
const std = @import("std");
const User = struct {
id: u64,
name: []const u8,
active: bool,
};
pub fn main() void {
const info = @typeInfo(User);
switch (info) {
.@"struct" => |struct_info| {
std.debug.print("field count = {}\n", .{
struct_info.fields.len,
});
for (struct_info.fields) |field| {
std.debug.print(
"field: {s}\n",
.{field.name},
);
}
},
else => {},
}
}Output:
field count = 3
field: id
field: name
field: activeThis is real reflection.
The compiler is exposing the structure of the type to your code.
Why switch Is Needed
Remember that @typeInfo returns a tagged union.
A type might be:
a struct
an enum
a pointer
a function
an integerSo Zig requires you to handle the correct case.
switch (info) {
.@"struct" => |struct_info| {
// struct handling
},
else => {},
}This is safer than assuming the type is always a struct.
Accessing Field Types
Each field contains more than just a name.
You can inspect the field type too.
const std = @import("std");
const User = struct {
id: u64,
name: []const u8,
active: bool,
};
pub fn main() void {
const info = @typeInfo(User);
switch (info) {
.@"struct" => |struct_info| {
for (struct_info.fields) |field| {
std.debug.print(
"{s}: {}\n",
.{
field.name,
field.type,
},
);
}
},
else => {},
}
}Possible output:
id: u64
name: []const u8
active: boolNow the program knows both:
field names
field typesat compile time.
Compile-Time Loops with Reflection
Reflection becomes much more powerful when combined with comptime.
Example:
inline for (struct_info.fields) |field| {
// compile-time loop
}An inline for loop runs during compilation.
That means Zig can generate specialized code for every field.
This is how many generic systems work in Zig.
Building a Generic Printer
Let us build a tiny generic debug printer.
const std = @import("std");
fn printStruct(value: anytype) void {
const T = @TypeOf(value);
const info = @typeInfo(T);
switch (info) {
.@"struct" => |struct_info| {
std.debug.print("{s} {{ ", .{
@typeName(T),
});
inline for (struct_info.fields, 0..) |field, i| {
if (i != 0) {
std.debug.print(", ", .{});
}
std.debug.print("{s}=", .{
field.name,
});
std.debug.print(
"{}",
.{@field(value, field.name)},
);
}
std.debug.print(" }}\n", .{});
},
else => {
std.debug.print("not a struct\n", .{});
},
}
}
const User = struct {
id: u64,
name: []const u8,
};
pub fn main() void {
const user = User{
.id = 42,
.name = "Ada",
};
printStruct(user);
}Output:
User { id=42, name=Ada }This function works for many struct types automatically.
Understanding @field
This line is important:
@field(value, field.name)@field accesses a field dynamically using its name.
Normal access:
value.idReflection access:
@field(value, "id")The second form is useful when field names come from reflection data.
Getting Type Names
Zig provides:
@typeName(T)Example:
std.debug.print("{s}\n", .{
@typeName(u32),
});Output:
u32For structs:
User
Point
RectangleThis is useful for debugging, logs, serializers, and generated code.
Reflection with Enums
Reflection also works for enums.
const std = @import("std");
const Color = enum {
red,
green,
blue,
};
pub fn main() void {
const info = @typeInfo(Color);
switch (info) {
.@"enum" => |enum_info| {
for (enum_info.fields) |field| {
std.debug.print(
"{s}\n",
.{field.name},
);
}
},
else => {},
}
}Output:
red
green
blueThe compiler exposes enum metadata too.
Reflection with Tagged Unions
Reflection becomes especially powerful with tagged unions.
const Shape = union(enum) {
circle: f32,
rectangle: struct {
width: f32,
height: f32,
},
};Reflection can inspect:
union fields
tags
payload typesThis allows advanced generic systems.
Reflection Is Type-Safe
This is one of Zig’s biggest advantages.
Many runtime reflection systems are string-heavy and weakly typed.
Example from dynamic systems:
"field does not exist"
"method missing"
"wrong runtime type"Zig reflection happens mostly during compilation.
If you use reflection incorrectly, the compiler usually catches it immediately.
Example:
@field(value, "missing_field")This produces a compile error if the field does not exist.
Reflection Does Not Mean Dynamic Typing
This is important.
Zig reflection does not turn Zig into a dynamic language.
Types remain static.
The compiler simply exposes information about those types during compilation.
Reflection in Zig is a compile-time metaprogramming tool.
Reflection and Code Generation
Reflection often replaces external code generators.
In some ecosystems, developers write:
schema generators
macro systems
template generators
codegen scriptsZig often avoids this because compile-time reflection can generate specialized code directly.
Example ideas:
automatic serializers
binary parsers
CLI parsers
network packet encoders
database row mappersAll from type information.
Reflection and Generic Programming
Reflection and generics work together naturally.
Suppose you want:
a generic serializer
a generic validator
a generic equality checkerReflection lets the compiler inspect arbitrary types.
comptime lets the compiler generate specialized code for those types.
This is one of the central design patterns in advanced Zig.
Reflection Is Not Free
Compile-time reflection has costs.
Heavy metaprogramming can increase:
compile times
compiler memory usage
code complexityBad reflection systems can become difficult to understand.
Example:
// Many layers of comptime reflection.
// Hard to debug.
// Hard to read.A good rule:
Use reflection to remove duplication.
Do not use reflection to hide logic.Prefer Simple Explicit Code First
Reflection is powerful, but explicit code is often better.
Good use:
automatic serializers
generic containers
debug systems
testing utilitiesBad use:
building a giant hidden framework
replacing simple functions with metaprogramming
making code harder to traceZig values clarity.
Reflection should simplify systems, not obscure them.
Reflection and the Zig Philosophy
Reflection in Zig reflects the overall language philosophy.
Zig wants:
explicit behavior
compile-time safety
low runtime cost
simple generated codeInstead of large runtime object systems, Zig exposes compile-time type information directly to the programmer.
You can inspect types, generate code, and build abstractions without hiding what the program is doing.
A Mental Model
Think of reflection like this:
Types are data.
The compiler can inspect that data.
Your compile-time code can react to that data.This is the foundation of many advanced Zig systems.
When you combine:
@typeInfo
comptime
inline for
@field
@TypeOf
@typeNameyou gain the ability to write flexible generic systems while still producing efficient static machine code.