Validation without a reflection panic
I wrote a small validator that accepted interface{} and walked fields marked validate:"required". The happy path worked, so I naturally declared victory before trying a pointer, a nil pointer, or anything that was not a struct.
The first version began with this:
v := reflect.ValueOf(x)
for i := 0; i < v.NumField(); i++ {
// inspect v.Field(i)
}
Passing &User{} produced reflect: call of reflect.Value.NumField on ptr Value. Recovering the panic would hide the real question: what inputs does this validator promise to accept? I chose structs and non-nil pointers to structs, with ordinary errors for everything else.
func validate(x interface{}) error {
v := reflect.ValueOf(x)
if !v.IsValid() {
return errors.New("cannot validate nil")
}
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return errors.New("cannot validate nil pointer")
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return fmt.Errorf("cannot validate %s", v.Kind())
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
if t.Field(i).Tag.Get("validate") != "required" {
continue
}
if isZero(v.Field(i)) {
return fmt.Errorf("%s is required", t.Field(i).Name)
}
}
return nil
}
The harder policy is what “required” means. For this validator it rejects empty strings, numeric zero, false, and nil pointers, maps, slices, and interfaces. That makes false invalid, so a required boolean is usually a design smell; a pointer can represent the distinct states absent, false, and true.
I ignore unexported fields. A general validator should not turn another package’s private representation into public input policy.
Nested structs are not automatically validated. A field gets nested validation only when its tag asks for it; otherwise adding a private detail to a child type could unexpectedly change the parent’s contract. Nil optional pointers remain acceptable, while a required nil pointer fails at its own field path.
Errors report the Go field path as well as the failed rule. A bare “required” is compact but useless once validation descends into two nested structs with similarly named fields.
Reflection removes field-by-field plumbing, but the useful part is still the contract: accepted top-level shapes, the meaning of “required,” whether nesting is opt-in, and useful error paths. Once those choices are explicit, the reflective code can stay small and unsurprising.