141 lines
3.9 KiB
Markdown
141 lines
3.9 KiB
Markdown
# Viper Unmarshal and Struct Mapping
|
|
|
|
## Basic Unmarshal
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// ✓ 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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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.
|