confkit vs Viper
Two different philosophies: confkit is typed and struct-first; Viper is dynamic and flexible. Here's how they compare in practice.
Quick Answer
confkit — typed generics API, built-in validation, secret redaction, 2 dependencies. Best for services that want config to be correct at startup.
Viper — dynamic string-keyed access, no validation, 30+ dependencies. Best for tools that need to read arbitrary keys at runtime or already use the Viper ecosystem.
Type Safety
The most fundamental difference: confkit uses Go generics to guarantee you get back the exact struct you defined. Viper returns interface{} under the hood, casting at read time.
cfg, err := confkit.Load[Config]( confkit.FromYAML("config.yaml"), confkit.FromEnv(), ) // cfg.Port is int — guaranteed // cfg.DB.URL is string — guaranteed // All fields validated, defaults applied
viper.SetConfigFile("config.yaml") viper.AutomaticEnv() viper.ReadInConfig() port := viper.GetInt("port") // Returns 0 if missing — silent failure // No validation, no error
With confkit, a wrong type or missing required field is caught at startup in Load(). With Viper, silent zero values propagate into your running service.
Validation
Viper has no built-in validation. confkit validates every field automatically using struct tag rules.
type Config struct { Port int `env:"PORT" default:"8080" validate:"min=1,max=65535"` Mode string `env:"MODE" validate:"oneof=dev prod staging"` DB string `env:"DATABASE_URL" validate:"required" secret:"true"` } // Load() returns error if any rule fails
// Viper has no validation tags. // Write it yourself after every read: port := viper.GetInt("port") if port < 1 || port > 65535 { log.Fatal("port out of range") } mode := viper.GetString("mode") if mode != "dev" && mode != "prod" { log.Fatal("invalid mode") }
Error Messages
Viper silently returns zero values for missing keys. confkit returns structured errors with field name, rule, value, and source.
Invalid configuration:
Port
error: must be between 1 and 65535
got: 99999
source: env (PORT)
DB
error: field is required
source: env (DATABASE_URL)// Missing key: returns 0, "", false
// No error. Service starts broken.
// If you use Unmarshal:
// mapstructure decode error:
// 1 error(s) decoding:
// '' expected type 'int', got 'string'Dependencies
| Module | confkit | Viper |
|---|---|---|
| Core dependencies | 2 (yaml.v3, go-toml) | 30+ bundled |
| Cloud sources | separate opt-in modules | bundled in core |
| fsnotify (file watching) | only if using watcher | always included |
| mapstructure | not used | core dependency |
| cast | not used | core dependency |
Viper bundles everything — even if you never use remote config, encryption, or file watching, the dependencies ship. confkit's cloud integrations (Vault, AWS, Kubernetes) are separate go gets.
Secret Redaction
type Config struct { APIKey string `env:"API_KEY" secret:"true"` } // Error output: API_KEY=*** (redacted) // Dump output: API_KEY=*** (redacted) // Log output: API_KEY=*** (redacted)
// Viper has no secret redaction.
// Passwords appear in plain text
// in logs, errors, and debug output.
//
// You must handle this yourself.Hot Reload
watcher, err := confkit.LoadWithWatcher[Config]( sources, func(cfg Config, err error) { if err != nil { // validation failed — keep old config return } // cfg is validated and typed atomic.StorePointer(¤t, cfg) }, )
Re-runs full load + validation on change. Bad config rejected before callback fires.
viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { // Fires on any change // No validation // You re-read values manually port := viper.GetInt("port") })
Fires on file change. No re-validation. Invalid config can propagate to a running service.
Full Comparison Table
| Feature | confkit | Viper |
|---|---|---|
| API style | Generic Load[T] → typed struct | String-keyed GetInt / GetString |
| Compile-time type safety | ✅ | ❌ |
| Built-in validation | ✅ struct tags | ❌ manual |
| Defaults | ✅ default:"..." tag | ⚠️ SetDefault() imperative |
| Secret redaction | ✅ secret:"true" tag | ❌ |
| Human-readable errors | ✅ field + rule + source | ❌ silent zeros or raw errors |
| YAML / JSON / TOML | ✅ | ✅ |
| Environment variables | ✅ | ✅ |
| CLI flags | ✅ | ✅ (pflag) |
| Vault | ✅ confkit/vault | ✅ viper/remote |
| AWS SSM / Secrets | ✅ confkit/aws | ⚠️ partial |
| Kubernetes ConfigMap | ✅ confkit/k8s | ❌ |
| Consul / etcd | ✅ opt-in modules | ✅ bundled |
| Hot reload | ✅ with re-validation | ✅ no re-validation |
| String interpolation | ✅ | ❌ |
| Schema generation | ✅ JSON Schema + Markdown | ❌ |
| Core dependencies | 2 | 30+ |
| Go version | 1.25.0+ (generics, range-over-func) | 1.17+ |
When to Choose
Choose confkit if…
- You want config validated and typed at service startup
- You need secret redaction in errors and logs
- You care about dependency footprint
- You want defaults and validation in struct tags, not imperative code
- You load from multiple sources (env + YAML + Vault + K8s)
- You want human-readable errors when deployment config is wrong
Choose Viper if…
- You need to read arbitrary keys dynamically at runtime
- You have an existing large Viper codebase
- You need the broader Viper ecosystem and community plugins
- You target Go 1.17 and below (pre-generics)
- You need
pflagintegration specifically