Files
teamai-test/.teamai/skills/common/golang-spf13-viper/references/unmarshal.md
T

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.