Config File Examples¶
Practical examples for loading configuration from files with BOA.
Basic Config File Loading¶
Tag a string field with configfile:"true" and BOA automatically loads the file before validation. CLI and env var values always take precedence over config file values.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
Host string `descr:"Server host" default:"localhost"`
Port int `descr:"Server port" default:"8080"`
Debug bool `descr:"Debug mode" optional:"true"`
}
func main() {
boa.CmdT[Params]{
Use: "server",
Short: "Start the server",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s\nPort: %d\nDebug: %v\n", p.Host, p.Port, p.Debug)
},
}.Run()
}
Create config.json:
# Uses config.json from default path:
$ go run .
Host: api.example.com
Port: 3000
Debug: true
# Point to a different config file:
$ go run . --config-file /etc/myapp/prod.json
Host: prod.example.com
Port: 443
Debug: false
# CLI flags override config file values:
$ go run . --port 9090
Host: api.example.com
Port: 9090
Debug: true
# No config file? No problem (it's optional):
$ go run . --config-file "" --host localhost
Host: localhost
Port: 8080
Debug: false
Config File with CLI Overrides¶
The value priority is: CLI flags > env vars > root config > substruct config > defaults > zero value.
package main
import (
"fmt"
"os"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"Server host" env:"APP_HOST" default:"localhost"`
Port int `descr:"Server port" env:"APP_PORT" default:"8080"`
}
func main() {
boa.CmdT[Params]{
Use: "server",
Short: "Demonstrate value priority",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s (source priority: CLI > env > config > default)\n", p.Host)
fmt.Printf("Port: %d\n", p.Port)
},
}.Run()
}
With config.json:
# Config file only:
$ go run . --config-file config.json
Host: from-config
Port: 3000
# Env overrides config:
$ APP_HOST=from-env go run . --config-file config.json
Host: from-env
Port: 3000
# CLI overrides everything:
$ APP_HOST=from-env go run . --config-file config.json --host from-cli
Host: from-cli
Port: 3000
Substruct Config Files¶
Nested structs can each have their own configfile:"true" field, loading from separate files. The root config overrides substruct configs when they overlap.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type DBConfig struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"Database host" default:"localhost"`
Port int `descr:"Database port" default:"5432"`
Name string `descr:"Database name" default:"mydb"`
}
type CacheConfig struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"Cache host" default:"localhost"`
Port int `descr:"Cache port" default:"6379"`
TTL int `descr:"Cache TTL seconds" default:"300"`
}
type Params struct {
ConfigFile string `configfile:"true" optional:"true"`
AppName string `descr:"Application name" default:"myapp"`
DB DBConfig
Cache CacheConfig
}
func main() {
boa.CmdT[Params]{
Use: "app",
Short: "Multi-config demo",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("App: %s\n", p.AppName)
fmt.Printf("DB: %s:%d/%s\n", p.DB.Host, p.DB.Port, p.DB.Name)
fmt.Printf("Cache: %s:%d (TTL=%ds)\n", p.Cache.Host, p.Cache.Port, p.Cache.TTL)
},
}.Run()
}
Create db.json:
Create cache.json:
Create app.json (root config -- overrides substruct values when fields overlap):
# Load all three config files:
$ go run . --config-file app.json --db-config-file db.json --cache-config-file cache.json
App: production-app
DB: db-primary.internal:5432/production
Cache: redis.internal:6379 (TTL=600s)
# Note: DB.Host is "db-primary.internal" (root config overrides db.json's "db.internal")
# DB.Port and DB.Name come from db.json since the root config didn't set them.
# Only substruct configs, no root config:
$ go run . --db-config-file db.json --cache-config-file cache.json
App: myapp
DB: db.internal:5432/production
Cache: redis.internal:6379 (TTL=600s)
# CLI overrides both configs:
$ go run . --db-config-file db.json --db-host cli-override
App: myapp
DB: cli-override:5432/production
Cache: localhost:6379 (TTL=300s)
Priority for substruct values: CLI > env > root config > substruct config > defaults.
Config Format Registry¶
JSON is the only format built in. Register additional formats with boa.RegisterConfigFormat.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.yaml"`
Host string `descr:"Server host"`
Port int `descr:"Server port" default:"8080"`
}
func main() {
// Register YAML support -- file extension determines which unmarshal to use
boa.RegisterConfigFormat(".yaml", yaml.Unmarshal)
boa.RegisterConfigFormat(".yml", yaml.Unmarshal)
boa.CmdT[Params]{
Use: "server",
Short: "Server with YAML config",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s, Port: %d\n", p.Host, p.Port)
},
}.Run()
}
Create config.yaml:
$ go run .
Host: api.example.com, Port: 3000
# Can also use JSON files -- BOA picks the right parser by extension:
$ go run . --config-file config.json
Host: ...
Override Unmarshal Per Command¶
Use ConfigUnmarshal on the command to bypass file extension detection entirely:
boa.CmdT[Params]{
Use: "server",
ConfigUnmarshal: yaml.Unmarshal, // Always use YAML regardless of file extension
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
// ...
},
}.Run()
Resolution order for choosing the unmarshal function:
- Explicit
ConfigUnmarshalon the command - Registered format matched by file extension (
.yaml->yaml.Unmarshal) json.Unmarshal(default fallback)
Config-File-Only Fields (boa:"ignore")¶
Fields tagged boa:"ignore" (or boa:"configonly") are not exposed as CLI flags or env vars. They only get populated from config files.
This is useful for complex settings that make sense in a file but not on the command line.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
Host string `descr:"Server host" default:"localhost"`
Port int `descr:"Server port" default:"8080"`
InternalID string `boa:"ignore"` // config file only
Metadata map[string]string `boa:"configonly"` // config file only (clearer alias)
Routes []RouteConfig `boa:"ignore"` // complex nested config
}
type RouteConfig struct {
Path string `json:"path"`
Backend string `json:"backend"`
}
func main() {
boa.CmdT[Params]{
Use: "server",
Short: "Server with config-only fields",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s:%d\n", p.Host, p.Port)
fmt.Printf("Internal ID: %s\n", p.InternalID)
fmt.Printf("Metadata: %v\n", p.Metadata)
for _, r := range p.Routes {
fmt.Printf("Route: %s -> %s\n", r.Path, r.Backend)
}
},
}.Run()
}
Create config.json:
{
"Host": "api.example.com",
"Port": 8080,
"InternalID": "svc-abc-123",
"Metadata": {
"version": "2.1.0",
"region": "us-east-1"
},
"Routes": [
{"path": "/api", "backend": "http://backend:3000"},
{"path": "/static", "backend": "http://cdn:8080"}
]
}
$ go run .
Host: api.example.com:8080
Internal ID: svc-abc-123
Metadata: map[region:us-east-1 version:2.1.0]
Route: /api -> http://backend:3000
Route: /static -> http://cdn:8080
# Host and Port can be overridden via CLI:
$ go run . --host localhost --port 3000
Host: localhost:3000
Internal ID: svc-abc-123
...
# InternalID and Routes are NOT available as CLI flags:
$ go run . --internal-id foo
# Error: unknown flag: --internal-id
Ignored Sub-Structs¶
You can also ignore an entire sub-struct. Its fields will not appear as CLI flags, but the struct is still populated from config files.
type DBConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Password string `json:"password"`
}
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
AppName string `descr:"App name"`
DB DBConfig `boa:"ignore"` // entire struct is config-only
}
Auto-Discovery with boaviper¶
The boaviper package provides Viper-like automatic config file discovery. It searches standard paths for config files without requiring the user to specify --config-file.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/GiGurra/boa/pkg/boaviper"
"github.com/spf13/cobra"
)
type Params struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"Server host" default:"localhost"`
Port int `descr:"Server port" default:"8080"`
Debug bool `descr:"Debug mode" optional:"true"`
}
func main() {
boa.CmdT[Params]{
Use: "myapp",
Short: "App with auto-discovery",
InitFunc: boaviper.AutoConfig[Params]("myapp"),
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Config: %s\nHost: %s, Port: %d, Debug: %v\n",
p.ConfigFile, p.Host, p.Port, p.Debug)
},
}.Run()
}
boaviper.AutoConfig searches these paths (first match wins):
./myapp.json(current directory)$HOME/.config/myapp/config.json/etc/myapp/config.json
All registered config format extensions are tried at each path (e.g., .json, .yaml if registered).
# Auto-discovers ./myapp.json:
$ echo '{"Port": 9090}' > myapp.json
$ go run .
Config: myapp.json
Host: localhost, Port: 9090, Debug: false
# Explicit --config-file overrides auto-discovery:
$ go run . --config-file /etc/myapp/prod.json
Config: /etc/myapp/prod.json
...
# No config file found? Uses defaults:
$ rm myapp.json
$ go run .
Config:
Host: localhost, Port: 8080, Debug: false
Custom Search Paths¶
This searches:
./config/myapp.json/opt/myapp/etc/config.json/opt/myapp/etc/myapp.json
Auto-Discover with Env Prefix¶
Combine auto-discovery with prefixed environment variables for a fully Viper-like experience:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/GiGurra/boa/pkg/boaviper"
"github.com/spf13/cobra"
)
type Params struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"Server host" default:"localhost"`
Port int `descr:"Server port" default:"8080"`
}
func main() {
boa.CmdT[Params]{
Use: "myapp",
Short: "Viper-like CLI",
// Auto-discover config files
InitFunc: boaviper.AutoConfig[Params]("myapp"),
// Prefix all env vars with MYAPP_
ParamEnrich: boa.ParamEnricherCombine(
boa.ParamEnricherDefault,
boaviper.SetEnvPrefix("MYAPP"),
),
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s, Port: %d\n", p.Host, p.Port)
},
}.Run()
}
# All three sources work:
$ MYAPP_PORT=3000 go run .
Host: localhost, Port: 3000
# Priority: CLI > env > config file > default
$ echo '{"Port": 5000}' > myapp.json
$ MYAPP_PORT=3000 go run . --port 9090
Host: localhost, Port: 9090
Explicit Config File Loading¶
For full control over config file loading, use boa.LoadConfigFile in a PreValidateFunc hook. This is useful when you need to load into a sub-struct or apply custom logic.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type AppConfig struct {
Host string
Port int
}
type Params struct {
ConfigFile string `descr:"Path to config file" optional:"true"`
AppConfig // embedded -- fields become --host, --port
}
func main() {
boa.CmdT[Params]{
Use: "app",
Short: "Explicit config loading",
PreValidateFunc: func(p *Params, cmd *cobra.Command, args []string) error {
// Load config file into the embedded AppConfig
return boa.LoadConfigFile(p.ConfigFile, &p.AppConfig, nil)
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s, Port: %d\n", p.Host, p.Port)
},
}.Run()
}
LoadConfigFile signature:
filePath: path to the config file (empty string is a no-op)target: pointer to the struct to populateunmarshalFunc: custom unmarshal function (niluses file extension detection, then falls back tojson.Unmarshal)
Mixed Config Formats¶
Different config files can use different formats. The format is detected by file extension when using the registry.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
type DBConfig struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"DB host" default:"localhost"`
Port int `descr:"DB port" default:"5432"`
}
type Params struct {
ConfigFile string `configfile:"true" optional:"true"`
AppName string `descr:"App name" default:"myapp"`
DB DBConfig
}
func main() {
// Register YAML in addition to built-in JSON
boa.RegisterConfigFormat(".yaml", yaml.Unmarshal)
boa.CmdT[Params]{
Use: "app",
Short: "Mixed format configs",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("App: %s\nDB: %s:%d\n", p.AppName, p.DB.Host, p.DB.Port)
},
}.Run()
}
# Root config as YAML, DB config as JSON:
$ go run . --config-file app.yaml --db-config-file db.json
App: from-yaml
DB: db-host:5432
BOA picks the correct unmarshal function based on each file's extension independently.