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:

TagMeaning
requiredValue must be non-zero.
omitemptySkip 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 cMust equal one of the space-separated values.
eq= / ne=Equal / not equal to a value.
emailValid email address.
url / uri / http_urlValid URL / URI.
hostname / fqdnValid host name.
ip / ipv4 / ipv6 / cidrIP address / CIDR.
uuid / uuid4Valid UUID.
alpha / alphanum / numericCharacter-class checks.
contains= / startswith= / endswith=Substring checks.
e164Phone number in E.164 form.
datetime=2006-01-02Parseable with the given Go time layout.
diveDescend into a slice/map/array and apply the following rules to each element.
eqfield=Other / nefield=OtherCompare 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 / Watch return a *ValidationError and the zero value. Startup fails fast.
  • At runtime: the update is discarded, Get() is unchanged, and OnError receives 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)
	}
})