Reflection
Reflection helps you write code that inspects its own structure at compile time and reports information about types. This makes it possible to build features like structural validation, automatic comparisons, serialization, safer assertions, and richer error messages without hardcoding details for specific type implementations.
Why reflection matters
Reflection is often used for serialization, such as converting values to JSON or MessagePack regardless of type. It also supports common structural tasks. Instead of writing equality, hashing, or copy logic by hand, reflection can apply those operations to all fields automatically.
In Mojo, reflection happens entirely at compile time. The compiler uses type information to generate specialized code, which avoids runtime cost while keeping the code flexible.
With this in mind, this page will introduce reflection through examples, so you can see working code to study and use. Reflection code is intentionally parameterized and compile-time heavy. The examples below show the patterns you can use in real reflection-based code.
Example: Present a type
This example demonstrates how to use reflection to inspect a type at compile time. It shows how to retrieve a type’s name and iterate over a struct’s fields, including their names and types. This pattern is the foundation for most reflection-based utilities, such as validation, copying, and equality checks.
Compile-time inputs
Mojo reflection APIs run at compile time and accept compile-time inputs.
Although reflection happens during compile time, it can drive behavior for runtime values. Knowing a value's type opens the door to retrieving type-specific metadata such as field counts, names, and types. That metadata can then be used to safely access and manipulate fields on concrete instances at runtime, as long as the access itself is guided by compile-time information.
Reflection calls
The reflection calls used in the following example include:
get_type_name[](): Returns the name of a type.struct_field_count[](),struct_field_names[](), andstruct_field_types[](): Return the count, names, and types of a struct's fields.
from reflection import (
struct_field_count, struct_field_names,
get_type_name, struct_field_types
)
fn show_type[T: AnyType]():
comptime type_name = get_type_name[T]()
comptime field_count = struct_field_count[T]() # count of fields
comptime field_names = struct_field_names[T]() # indexed list of field names
comptime field_types = struct_field_types[T]() # indexed list of field types
print("struct", type_name)
@parameter
for idx in range(field_count):
comptime field_name = field_names[idx]
comptime field_type = get_type_name[field_types[idx]]()
var intro = "├──" if idx < (field_count - 1) else "└──"
print(intro, " var ", field_name, ": ", field_type, sep="")
@fieldwise_init
struct MyStruct:
var x: String
var y: Optional[Int]
comptime DefaultItemCount = 10
struct ParameterizedStruct[T: Copyable, item_count: Int = DefaultItemCount](Copyable):
var list: List[Self.T]
fn __init__(out self):
self.list = List[Self.T](capacity=Self.item_count)
fn main():
show_type[MyStruct]()
show_type[Optional[Float64]]()
show_type[Dict[Int, String]]()
show_type[ParameterizedStruct[String, item_count=5]]()Insights
- When working with reflection, you must use parameterized types in the square
parameter brackets for calls that use reflection features, such as
show_type[](). These elements can be concrete types likeMyStructor generic type parameters likeT. - This example uses a
T: AnyTypeparameter for theshow_type[]()function. This is the least restrictive constraint and allows the function to work with any type passed at the call site. - Reflection functions are themselves parameterized.
That means you do not call
foo(). You callfoo[T]()to reflect on a type. - All processing of reflected information happens at compile time.
For that reason, these reflection examples use
@parameter forand@parameter if.- In this example,
@parameter forallows the loop index to be used across iterations. - Later examples show
@parameter if.
- In this example,
Program output
The following output shows the result of calling show_type[]() on three
different types in main(). Each call prints the compiler’s view of the type,
including its fully resolved name and the structure of its fields:
struct reflect.MyStruct
├── var x: String
└── var y: std.collections.optional.Optional[Int]
struct std.collections.optional.Optional[SIMD[DType.float64, 1]]
└── var _value: std.utils.variant.Variant[<unprintable>]
struct std.collections.dict.Dict[Int, String, std.hashlib._ahash.
AHasher[[0, 0, 0, 0] : SIMD[DType.uint64, 4]]]
├── var _len: Int
├── var _n_entries: Int
├── var _index: std.collections.dict._DictIndex
└── var _entries: List[std.collections.optional.Optional[std.collections.
dict.DictEntry[Int, String, std.hashlib._ahash.
AHasher[[0, 0, 0, 0] : SIMD[DType.uint64, 4]]]]]
struct show_type.ParameterizedStruct[String, 5]
└── var list: List[String]When comparing the type names at the call sites with the names shown here,
keep in mind that this output reflects how the compiler represents the type.
This includes defaulted parameters, such as the dictionary’s Hasher.
Comptime type aliases are expanded, so Float64 displays
as SIMD[DType.float64, 1].
This example showed how to read type information. The next examples show how to use that information to manipulate struct instances.
Example: Copying data
This example shows how reflected field information can drive real behavior. It uses reflection to copy data from one instance to another by iterating over fields and checking which ones are safe to copy. This pattern demonstrates how reflection enables generic, type-safe operations without hardcoding field access.
When a struct conforms to MakeCopyable, it gains the copy_to() method that
uses reflection to perform the copy. Like all methods, you call it from an
instance. For this method, you provide it with target instance.
Its behavior is similar in spirit to ImplicitlyCopyable, but copy_to()
limits copying to fields whose types conform to Copyable. It requires an
already initialized target, avoiding matching values to __init__ arguments.
from reflection import struct_field_count, struct_field_types
trait MakeCopyable:
fn copy_to(self, mut other: Self):
comptime field_count = struct_field_count[Self]()
comptime field_types = struct_field_types[Self]()
@parameter
for idx in range(field_count):
comptime field_type = field_types[idx]
# Guard: field type must be copyable
@parameter
if not conforms_to(field_type, Copyable): continue
# Perform the copy
ref p_value = __struct_field_ref(idx, self)
__struct_field_ref(idx, other) = p_valueInsights
- The function iterates over reflected fields and checks each one for
Copyableconformance, skipping any field that cannot be copied. - As a method,
copy_to()does not require a type parameter such ascopy_to[T](). It has direct access toSelf, which is enabled byMakeCopyabletrait adoption. - The
copy_to()implementation is heavily parameterized and evaluated at compile time. It uses@parameter forand@parameter iftogether with reflection calls. - The
__struct_field_ref(idx, self)and__struct_field_ref(idx, other)calls return references to fields by index, which allows both reading from the source instance and writing to the destination instance.
Create a struct
The following MultiType struct conforms to MakeCopyable meaning you can
call copy_to on its instances:
@fieldwise_init
struct MultiType(Writable, MakeCopyable):
var w: String
var x: Int
var y: Bool
var z: Float64
fn write_to[W: Writer](self, mut writer: W):
writer.write("[{}, {}, {}, {}]".format(self.w, self.x, self.y, self.z))Use the copying functionality
Demonstrating the behavior, this main() function creates two instances,
one populated by normal values, and one essentially zeroed-out.
After copying, target_instance has received its values from
original_instance's fields:
fn main():
var original_instance = MultiType("Hello", 1, True, 2.5)
var target_instance = MultiType("", 0, False, 0.0)
print("Original instance:", original_instance) # "Hello", 1, True, 2.5
print("Target instance before: ", target_instance) # "", 0, False, 0.0
original_instance.copy_to(target_instance)
print("Target instance after: ", target_instance) # "Hello", 1, True, 2.5Example: Testing equality
This example demonstrates how reflection can be used to implement structural equality. Compile-time loop unrolling, conformance checks, and reflection drive runtime comparisons. This is the same pattern used for generic equality, hashing, and validation logic.
In each iteration, test_equality checks for Equatable conformance and
retrieves field value references from the lhs and rhs arguments. It uses
early return for the first inequality (False), otherwise returning True:
from reflection import struct_field_count, struct_field_types
fn test_equality[T: AnyType](lhs: T, rhs: T) -> Bool:
comptime field_count = struct_field_count[T]()
comptime field_types = struct_field_types[T]()
@parameter
for idx in range(field_count):
# Guard: field type must be equatable
comptime field_type = field_types[idx]
@parameter
if not conforms_to(field_type, Equatable): continue
# Fetch values
ref lhs_value = __struct_field_ref(idx, lhs)
ref rhs_value = __struct_field_ref(idx, rhs)
# Early exit with `False` when inequality found
if trait_downcast[Equatable](lhs_value) != trait_downcast[Equatable](rhs_value): return False
return TrueInsights
- This example uses two key elements to ensure smooth operation:
conforms_to()andtrait_downcast[](). conforms_to()ensures each field's type isEquatableand therefore works with the inequality operator (!=).trait_downcastis used with parameter input types. It rebinds a typed value, returning a value that conforms to the specifiedTrait. If its type, after resolution, does not conform to that trait, it will produce a compilation error. Checking for conformance first ensures that error won't occur.- As its name suggests,
__struct_field_ref()is limited to use with struct types.
Calling the tests
To demonstrate this function, the following main() first
copies values (equal), and then mutates original_instance
and tests again (inequal):
fn main():
var original_instance = MultiType("Hello", 1, True, 2.5)
var target_instance = MultiType("", 0, False, 0.0)
original_instance.copy_to(target_instance)
print("Values equal" if \
test_equality(original_instance, target_instance) \
else "Values not equal") # Values equal
original_instance.z = 42.0
print("Values equal" if \
test_equality(original_instance, target_instance) \
else "Values not equal") # Values not equalLearn more
- Visit the reflection package documentation to explore additional Mojo reflection capabilities.
- Learn more about traits.
- (Coming soon): Dive into generics, trait conformance tests with
conforms_to(), and downcasts withtrait_downcast[]().
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!