DocsStructsValidation

Validation

Check inputs against per-field rules and report failures without mutating the struct.

Validate runs the configured rules over the inputs and reports which fields failed which rules. It never mutates the struct, so you can validate first and only Set once the inputs are known good. It returns a map[string][]string, keyed by field name, holding the messages for each failing field. An empty map means everything passed.

type Args struct {
	Name   string `json:"name" rules:"required"`
	Format string `json:"format" rules:"oneof:json,yaml,toml"`
}

manager := structs.New(&Args{}, structs.WithTags("json"))

errs, err := manager.Validate(map[string]any{
	"format": "xml", // not one of json,yaml,toml
	// name omitted, so the required rule fires
})
// err is nil (validation ran cleanly)
// errs == map[string][]string{
//   "name":   {"required"},
//   "format": {"must be one of: json, yaml, toml"},
// }

The returned error is separate from the field messages. It is non-nil only when validation could not run (a reflection failure, say), not when a field fails a rule. Field failures always come back in the map. So the usual shape is to check the error, then check the map length.

if errs, err := manager.Validate(inputs); err != nil {
	return fmt.Errorf("failed to validate config: %w", err)
} else if len(errs) > 0 {
	return fmt.Errorf("config is invalid: %v", errs)
}

Built-in rules

Rules are declared per field in the rules: tag, separated by spaces, with arguments after a colon. Two rules are built in.

  • required fails when a field has no non-empty input, no default, and a zero current value. Whitespace-only string input counts as empty.
  • oneof:a,b,c restricts a field to one of the listed values. An empty or absent value passes on its own, so pair it with required to force both presence and membership. When a default is set, the default is what gets checked if no input arrives.
Port     int    `rules:"required"`
LogLevel string `default:"info" rules:"oneof:debug,info,warn,error"`
Format   string `rules:"required oneof:json,yaml,toml"` // both

Custom rules

Register your own named rules with WithRules. A rule is a RuleFunc, keyed by the name used in the tag. Pass a fresh map to replace the built-in set, or start from structs.DefaultRules to extend it.

rules := map[string]structs.RuleFunc{
	"required": structs.Required, // keep the built-ins
	"oneof":    structs.OneOf,
	"even": func(field string, values map[string]any, def string, v reflect.Value, args []string) (map[string][]string, error) {
		if n, _ := utils.ToInt(values[field]); n%2 != 0 {
			return map[string][]string{field: {"must be even"}}, nil
		}
		return nil, nil
	},
}

manager := structs.New(cfg, structs.WithRules(rules))

A RuleFunc returns the field-to-messages map for any failure and a non-nil error only for an internal problem, not a validation miss. The full signature and each built-in are in the rules reference.

Note

By default the failing field is reported under its tag-priority-resolved name. Use WithValidationTag to report under a different, stable tag instead, so the caller-facing error keys do not shift when you change the lookup order.