3.9 KiB
Viper Unmarshal and Struct Mapping
Basic Unmarshal
type Config struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
LogLevel string `mapstructure:"log_level"`
Database struct {
DSN string `mapstructure:"dsn"`
MaxConn int `mapstructure:"max_conn"`
} `mapstructure:"database"`
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return fmt.Errorf("decoding config: %w", err)
}
mapstructure tags
Always use mapstructure tags. Without them, mapstructure falls back to case-insensitive field name matching, which works for simple cases but silently fails for:
- Nested structs where the outer key uses an underscore (
max_conn→MaxConn) - Unexported fields
- Fields where the Go name does not match the config key
// ✓ Good — explicit and immune to rename surprises
type TLSConfig struct {
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
Enabled bool `mapstructure:"enabled"`
}
// ✗ Fragile — relies on case-folding; breaks when config key uses underscores
type TLSConfig struct {
CertFile string // viper key "certfile" or "CertFile", not "cert_file"
KeyFile string
Enabled bool
}
UnmarshalKey — extracting a sub-tree
type DatabaseConfig struct {
DSN string `mapstructure:"dsn"`
MaxConn int `mapstructure:"max_conn"`
}
var dbCfg DatabaseConfig
if err := viper.UnmarshalKey("database", &dbCfg); err != nil {
return fmt.Errorf("decoding database config: %w", err)
}
Prefer UnmarshalKey over viper.Sub + Unmarshal — fewer nil checks and less boilerplate.
time.Duration
Viper's GetDuration parses duration strings ("1h30m", "500ms") from config files and env vars. When using Unmarshal, mapstructure does not know how to decode a duration string into time.Duration by default.
Register a decode hook:
import "github.com/mitchellh/mapstructure"
var cfg Config
err := viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) {
dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
dc.DecodeHook,
)
})
StringToTimeDurationHookFunc handles "1h30m" → time.Duration. StringToSliceHookFunc(",") handles "a,b,c" → []string{"a", "b", "c"}.
net.IP and custom types
import "github.com/mitchellh/mapstructure"
func stringToIPHookFunc() mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String || t != reflect.TypeOf(net.IP{}) {
return data, nil
}
ip := net.ParseIP(data.(string))
if ip == nil {
return nil, fmt.Errorf("invalid IP address: %s", data)
}
return ip, nil
}
}
Squash for embedded structs
type BaseConfig struct {
LogLevel string `mapstructure:"log_level"`
Debug bool `mapstructure:"debug"`
}
type ServerConfig struct {
BaseConfig `mapstructure:",squash"` // merge BaseConfig fields at this level
Port int `mapstructure:"port"`
}
Without ,squash, the base config must be nested under a baseconfig key in the config file.
Remain for unknown keys
type Config struct {
Port int `mapstructure:"port"`
Remain map[string]interface{} `mapstructure:",remain"`
}
Extra keys from the config file are collected in Remain instead of being silently dropped. Useful for forward compatibility.
Weak decoding
Enable weak type decoding (e.g., string "true" → bool true) when working with env vars that are always strings:
var cfg Config
err := viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) {
dc.WeaklyTypedInput = true
})
Use with caution — weak decoding can hide bugs where a wrong value type is silently converted.