Validation
Add a validate tag to any field and mamori enforces it - on the initial Load and on every reconciled update. If a new value would make the struct invalid, the update is rejected atomically: Get() keeps returning the last good config and OnError receives a *ValidationError.
type Config struct {
LogLevel string `source:"env:LOG_LEVEL" default:"info" validate:"oneof=debug info warn error"`
Workers int `source:"env:WORKERS" default:"4" validate:"gte=1,lte=256"`
AdminURL string `source:"env:ADMIN_URL" validate:"omitempty,url"`
Port int `source:"env:PORT" validate:"required,min=1,max=65535"`
}
Which validators are available
The validate tag is go-playground/validator/v10 syntax - the same library used by Gin, Echo, and many others. Any validator it ships works here. The most common ones:
| Tag | Meaning |
|---|---|
required |
Value must be non-zero. |
omitempty |
Skip the remaining rules if the value is empty. |
gte= / lte= / gt= / lt= |
Numeric (or length) bounds, e.g. gte=1,lte=256. |
min= / max= / len= |
Length for strings/slices/maps, value for numbers. |
oneof=a b c |
Must equal one of the space-separated values. |
eq= / ne= |
Equal / not equal to a value. |
email |
Valid email address. |
url / uri / http_url |
Valid URL / URI. |
hostname / fqdn |
Valid host name. |
ip / ipv4 / ipv6 / cidr |
IP address / CIDR. |
uuid / uuid4 |
Valid UUID. |
alpha / alphanum / numeric |
Character-class checks. |
contains= / startswith= / endswith= |
Substring checks. |
e164 |
Phone number in E.164 form. |
datetime=2006-01-02 |
Parseable with the given Go time layout. |
dive |
Descend into a slice/map/array and apply the following rules to each element. |
eqfield=Other / nefield=Other |
Compare against another field on the struct. |
Combine rules with commas (AND): validate:"required,gte=1,lte=256". The full catalogue is in the validator docs.
Nested structs and slices
Validation runs over the whole decoded struct, so nested structs are validated too. Use dive to validate slice or map elements:
type Config struct {
Redis RedisConfig `source:"aws-sm://prod/redis" flatten:"json"` // RedisConfig's own validate tags run
Origins []string `source:"env:ORIGINS" validate:"required,dive,url"` // each origin must be a URL
}
Custom validation
Swap the validator entirely with WithValidator. Any type implementing Validate(any) error works, so you can register custom go-playground validators, or plug in a different engine:
v := validator.New(validator.WithRequiredStructEnabled())
_ = v.RegisterValidation("dns1123", func(fl validator.FieldLevel) bool {
return isDNS1123Label(fl.Field().String())
})
cfg, err := mamori.Load[Config](ctx,
mamori.WithValidator(myAdapter{v}), // Validate(any) error wrapping v.Struct
)
What failure looks like
- At load:
Load/Watchreturn a*ValidationErrorand the zero value. Startup fails fast. - At runtime: the update is discarded,
Get()is unchanged, andOnErrorreceives a*ValidationError. Unwrap it to reach the underlying validator error:
mamori.OnError(func(err error) {
var ve *mamori.ValidationError
if errors.As(err, &ve) {
log.Printf("rejected invalid config update: %v", ve)
}
})