Every source shares the one cfg tag; the source name is what you target
in a per-source override (cfg:"port,env:DB_PORT") and what appears in the
resolution trace.
| Source name | Reads from | Module | Requires | Key from base name |
|---|---|---|---|---|
default |
Literal gos:"default:..." value |
core | — | — (uses gos) |
env |
Environment variable | core | — | SCREAMING_SNAKE |
json |
JSON file | core | GOSTRUCTOR_JSON=path/to/file.json |
name as written; . nests |
ini |
INI file | core | GOSTRUCTOR_INI=path/to/file.ini |
global key; section#key overrides |
yaml |
YAML file | gostructor/yaml |
GOSTRUCTOR_YAML=path/to/file.yml |
name as written; . nests |
toml |
TOML file | gostructor/toml |
GOSTRUCTOR_TOML=path/to/file.toml |
top-level key; table#key overrides |
hocon |
HOCON file | gostructor/hocon |
GOSTRUCTOR_HOCON=path/to/file.hocon |
name as written; . nests |
vault |
HashiCorp Vault secret | gostructor/vault |
VAULT_ADDR, VAULT_TOKEN |
override only (vault:path#key) |
Non-core sources must be added explicitly via WithSources — the core module
has no way to know they exist (that’s the whole point of the zero-dependency
core). The core default source list is just Env, Default; file and secret
sources are opt-in via WithSources, so a bare cfg base name never triggers
an unexpected file load.
type Config struct {
Retries int `gos:"default:3"`
Flags []bool `gos:"sep:|,default:true|false|true"`
}
gos:"default:<v>" is the literal default, resolved by the default source.
Slices split on the field separator (comma by default; a non-comma sep when
the value itself contains commas). There is no map syntax for defaults (see
field-types.md).
type Config struct {
Port int `cfg:"port"` // reads PORT
Legacy int `cfg:"legacy,env:OLD_PORT"` // reads OLD_PORT (override)
Signals []bool `cfg:"signals"` // reads SIGNALS=true,false,true
}
The env source turns the base name into SCREAMING_SNAKE_CASE (port → PORT,
maxConns → MAX_CONNS). Pin a different variable with an env: override.
type Config struct {
Host string `cfg:"host,json:server.host"`
Tags []int `cfg:"tags,json:server.tags"`
}
cfg, err := gostructor.Configure(&Config{},
gostructor.WithSources(gostructor.JSONFile("config.json")))
{"server": {"host": "0.0.0.0", "tags": [1, 2, 3]}}
A top-level key uses the base name (cfg:"host" → the host key); a nested
value is addressed with a dotted json: override (json:server.host), no
matching nested Go struct required. A bare object key like cfg:"server" on a
map[string]T field addresses the whole nested object.
INI uses section#key, since INI’s own structure is two levels (section, then
key). A base name reads the global, section-less part; a section value uses an
ini: override:
type Config struct {
Password string `cfg:"password,ini:database#password"`
}
[database]
password = secret
Each lives in its own module and mirrors JSON’s addressing style (YAML) or
INI’s (TOML/HOCON), targeted by its source name, and registered explicitly via
WithSources:
import "github.com/goreflect/gostructor/yaml"
type Config struct {
Host string `cfg:"host,yaml:server.host"`
Tags []string `cfg:"tags,yaml:server.tags"`
}
os.Setenv(yaml.FileEnvVar, "config.yml")
cfg, err := gostructor.Configure(&Config{}, gostructor.WithSources(yaml.New()))
server:
host: 0.0.0.0
tags:
- primary
- eu-west
gostructor/toml and gostructor/hocon ship hand-written parsers for a
practical subset of each format, not the full spec — see
limitations.md for exactly what’s out of scope before you
commit a config file that needs it.
Set VAULT_ADDR and VAULT_TOKEN (the same variables the vault CLI itself
uses), then give each field an explicit vault:path/to/secret#key override.
Vault has no name-based default — a secret path can’t be guessed from a
field name — so a field without a vault: override is simply skipped by the
vault source:
import "github.com/goreflect/gostructor/vault"
type Config struct {
APIKey string `cfg:"apiKey,vault:my-service/stage/creds#api-key" gos:"secret"`
RateLimit int16 `cfg:"rateLimit,vault:my-service/stage/limits#rate"`
Allowlist []int32 `cfg:"allowlist,vault:my-service/stage/net#allowlist"` // comma-separated secret value
}
cfg, err := gostructor.Configure(&Config{}, gostructor.WithSources(vault.New()))
Built on the official hashicorp/vault/api
client, not a third-party wrapper.
A field can be resolvable by several sources at once. There is no priority tag
and no global selector — priority is the order of the sources you pass to
WithSources. The first source that reports a value wins. To flip which
source leads for a whole config, reorder the slice:
// prod: an operator's env override leads; the default is the fallback.
gostructor.Configure(&cfg, gostructor.WithSources(gostructor.Env(), gostructor.Default()))
// dev: the baked-in default leads, so a stray env var is ignored.
gostructor.Configure(&cfg, gostructor.WithSources(gostructor.Default(), gostructor.Env()))
The same struct resolves differently purely from the list order — see
examples/priority for a runnable demo.
type Source interface {
Name() string
Resolve(field FieldContext) (value any, found bool, err error)
}
Name is the source’s identity (“yaml”) and its cfg override key. In
Resolve, ask the field for your key with
field.SourceKey("yaml", gostructor.Identity) — that returns the yaml:
override if present, else your naming strategy applied to the base name, else
"" when the field doesn’t apply to you. found=false means “nothing to
contribute for this field”, letting Configure fall through to the next source
instead of treating it as an error. Look at gostructor/yaml’s source.go for
a complete, short example — it’s about seventy lines including file loading and
error handling.