Load one or more config files with clear precedence and traceable parse steps
Glazed provides first-class support for reading configuration from one or more YAML/JSON files. Files are applied from low β high precedence, every file is recorded as its own parse step, and the result integrates cleanly with environment variables, positional args, and flags.
source: config and metadata such as { config_file, index }, and richer layered flows can also record { config_index, config_layer, config_source_name, config_source_kind }.FromFile, FromFiles), via resolved layered inputs (FromResolvedFiles), or directly from declarative plans (FromConfigPlan, FromConfigPlanBuilder).This guide shows how to load single and multiple files, integrate with Cobra, implement app-level file resolution patterns, use pattern- and custom-mappers, inspect parse steps, and validate config files.
Use this approach when youβre embedding Glazed into a service or library and you want explicit, programmatic control over where configuration comes from. You decide the exact order of sources and call the middleware execution yourself. This makes the precedence rules obvious in code and easy to unit test.
If you already have a declarative config.Plan, you can either resolve it yourself and call FromResolvedFiles(...), or load it directly with FromConfigPlan(...) / FromConfigPlanBuilder(...).
package main
import (
"github.com/go-go-golems/glazed/pkg/cmds/schema"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
"github.com/go-go-golems/glazed/pkg/cmds/fields"
"github.com/spf13/cobra"
)
func run(cmd *cobra.Command, args []string) error {
// Define sections
demo, _ := schema.NewSection(
"demo", "Demo",
schema.WithFields(
fields.New("api-key", fields.TypeString),
fields.New("threshold", fields.TypeInteger, fields.WithDefault(10)),
),
)
pls := schema.NewSchema(sections.WithSections(demo))
parsed := values.New()
// Apply middlewares in reverse-precedence order so later sources override earlier ones
err := sources.Execute(pls, parsed,
sources.FromDefaults(), // Defaults
sources.FromFiles([]string{"base.yaml"}), // Config (low β high)
sources.FromEnv("MYAPP"), // Env (prefix)
sources.FromArgs(args), // Positional args
sources.FromCobra(cmd), // Flags
)
return err
}
If youβre building a CLI, the Cobra integration keeps environment-variable loading and config-file loading separate on purpose. AppName turns on the env prefix for the built-in parser path, while ConfigPlanBuilder adds explicit config-file discovery. This keeps your command code focused on business logic while Glazed handles the parsing pipeline and debug flags (like --print-parsed-fields).
Use github.com/go-go-golems/glazed/pkg/cli to build Cobra commands and attach config processing. The default parser path wires env loading when AppName is set; config loading stays explicit through ConfigPlanBuilder. If you supply MiddlewaresFunc, you replace that built-in chain and must re-add any sources you still want.
Required fields are validated after source resolution in the Cobra parser path. A field declared with fields.WithRequired(true) can therefore be satisfied by config or env as well as by a Cobra flag. Missing required values still fail normal command execution, but --print-parsed-fields, --print-yaml, --print-schema, and --help do not run final required-value validation so diagnostics and help remain available while you are debugging configuration.
package main
import (
"github.com/go-go-golems/glazed/pkg/cli"
"github.com/go-go-golems/glazed/pkg/cmds"
"github.com/go-go-golems/glazed/pkg/cmds/schema"
"github.com/go-go-golems/glazed/pkg/cmds/fields"
)
func build() (*cobra.Command, error) {
demo, _ := schema.NewSection(
"demo", "Demo",
schema.WithFields(
fields.New("api-key", fields.TypeString),
fields.New("threshold", fields.TypeInteger, fields.WithDefault(10)),
),
)
desc := cmds.NewCommandDescription("demo", cmds.WithSectionsList(demo))
// AppName enables env loading in the default parser path (prefix = APPNAME_)
// ConfigPlanBuilder defines config-file discovery explicitly
return cli.BuildCobraCommandFromCommand(&DemoBare{desc},
cli.WithParserConfig(cli.CobraParserConfig{
AppName: "myapp",
ConfigPlanBuilder: func(_ *values.Values, _ *cobra.Command, _ []string) (*config.Plan, error) {
return config.NewPlan(
config.WithLayerOrder(config.LayerSystem, config.LayerCWD),
).Add(
config.ExplicitFile("base.yaml").Named("base").InLayer(config.LayerSystem),
config.ExplicitFile("local.yaml").Named("local").InLayer(config.LayerCWD),
), nil
},
}),
)
}
Load a single YAML/JSON file when your applicationβs configuration is centralized. The file is parsed into your field sections, and each value update is recorded as a config parse step, making it clear where settings came from when debugging.
package main
import (
"github.com/go-go-golems/glazed/pkg/cmds/schema"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
)
schema_ := schema.NewSchema(/* ... */)
parsed := values.New()
_ = sources.Execute(schema_, parsed,
sources.FromFile("/etc/myapp/config.yaml"),
)
Overlays let you compose configuration from multiple files with deterministic precedence. A common pattern is a base file committed to version control plus a local developer override. Glazed applies files in the order you provide (low β high), and the last writer wins. Each file is recorded as its own parse step with config_file and index metadata so you can audit how a value evolved.
package main
import (
"github.com/go-go-golems/glazed/pkg/cmds/schema"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
)
files := []string{"base.yaml", "env.yaml", "local.yaml"} // low β high precedence
_ = sources.Execute(schema_, parsed,
sources.FromFiles(files),
)
If your application needs more than βload these files in this order,β use a declarative config plan from glazed/pkg/config. This is the reusable API for expressing layered discovery such as system config, user config, repository-local config, working-directory config, and explicit overrides.
A plan is built from named source specs and an explicit layer order:
plan := config.NewPlan(
config.WithLayerOrder(
config.LayerSystem,
config.LayerUser,
config.LayerRepo,
config.LayerCWD,
config.LayerExplicit,
),
config.WithDedupePaths(),
).Add(
config.SystemAppConfig("myapp").Named("system-app-config"),
config.XDGAppConfig("myapp").Named("xdg-app-config"),
config.HomeAppConfig("myapp").Named("home-app-config"),
config.GitRootFile(".myapp.local.yaml").Named("git-root-local"),
config.WorkingDirFile(".myapp.local.yaml").Named("cwd-local"),
config.ExplicitFile(explicitPath).Named("explicit-config-file"),
)
files, report, err := plan.Resolve(context.Background())
if err != nil {
return err
}
fmt.Println(report.String())
err = sources.Execute(
schema_,
parsed,
sources.FromResolvedFiles(files),
sources.FromDefaults(fields.WithSource(fields.SourceDefaults)),
)
If you do not need the explicit files, report := plan.Resolve(...) step, you can load the plan directly:
err = sources.Execute(
schema_,
parsed,
sources.FromConfigPlan(plan),
sources.FromDefaults(fields.WithSource(fields.SourceDefaults)),
)
Use the direct plan middlewares when you want Glazed to resolve and load the plan inside the middleware pipeline. Use FromResolvedFiles(...) when you want to inspect, test, print, or otherwise reuse the explicit []ResolvedConfigFile output before loading.
Use this approach when you want both:
config_layer and config_source_nameSee the dedicated topic Declarative Config Plans and the runnable example cmd/examples/config-plan for the full pattern.
Many CLIs have a conventional config location (XDG, home dotdir, or /etc). Express that policy directly through config.Plan so the search order and precedence are visible in code. Pair plan-based discovery with the --config-file flag (already provided by the command-settings section) so power users can override discovery explicitly.
Use github.com/go-go-golems/glazed/pkg/config to build a plan that discovers a conventional app config plus explicit overrides:
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/go-go-golems/glazed/pkg/cli"
"github.com/go-go-golems/glazed/pkg/cmds/values"
glazedconfig "github.com/go-go-golems/glazed/pkg/config"
"github.com/spf13/cobra"
)
overrideSibling := func(path string) glazedconfig.SourceSpec {
return glazedconfig.SourceSpec{
Name: "explicit-override",
Layer: glazedconfig.LayerExplicit,
SourceKind: "computed-override-file",
Optional: true,
Discover: func(context.Context) ([]string, error) {
if strings.TrimSpace(path) == "" {
return nil, nil
}
stem := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
override := filepath.Join(filepath.Dir(path), fmt.Sprintf("%s.override.yaml", stem))
if _, err := os.Stat(override); err == nil {
return []string{override}, nil
} else if os.IsNotExist(err) {
return nil, nil
} else {
return nil, err
}
},
}
}
builder := func(parsed *values.Values, _ *cobra.Command, _ []string) (*glazedconfig.Plan, error) {
cs := &cli.CommandSettings{}
_ = parsed.DecodeSectionInto(cli.CommandSettingsSlug, cs)
return glazedconfig.NewPlan(
glazedconfig.WithLayerOrder(
glazedconfig.LayerSystem,
glazedconfig.LayerUser,
glazedconfig.LayerExplicit,
),
).Add(
glazedconfig.SystemAppConfig("myapp").Named("system-app-config"),
glazedconfig.XDGAppConfig("myapp").Named("xdg-app-config"),
glazedconfig.HomeAppConfig("myapp").Named("home-app-config"),
glazedconfig.ExplicitFile(cs.ConfigFile).Named("explicit-config"),
overrideSibling(cs.ConfigFile),
), nil
}
Glazed supports both βdefault-shapedβ configs (where the file mirrors your sections and fields) and mappers (which translate arbitrary structures to field updates). Use the default structure for greenfield projects and simple casesβitβs the most transparent. Reach for mappers when you must consume legacy formats, have nested structures that donβt match your field layout, or need to derive multiple fields from one subtree.
Glazed supports two ways to map config file data into field sections:
# default structure
demo:
api-key: cfg
threshold: 33
Pattern mappers describe how to traverse a config tree and map matched values into fields. Patterns support exact segments, wildcards, and named captures (for environment-like keys such as {env}). Validation happens both at construction time (syntax, capture references, static targets) and at runtime (required matches, ambiguity, collisions). Prefer named captures over wildcards when you expect multiple values to be collected.
Use github.com/go-go-golems/glazed/pkg/cmds/middlewares/patternmapper to declare mapping rules and pass the mapper to LoadFieldsFromFile.
package main
import (
"github.com/go-go-golems/glazed/pkg/cmds/schema"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
pm "github.com/go-go-golems/glazed/pkg/cmds/middlewares/patternmapper"
"github.com/go-go-golems/glazed/pkg/cmds/fields"
)
demo, _ := schema.NewSection("demo", "Demo",
schema.WithFields(
fields.New("api-key", fields.TypeString),
fields.New("dev-api-key", fields.TypeString),
fields.New("prod-api-key", fields.TypeString),
),
)
pls := schema.NewSchema(sections.WithSections(demo))
mapper, _ := pm.NewConfigMapper(pls,
pm.MappingRule{
Source: "app.{env}.settings",
TargetSection: "demo",
Rules: []pm.MappingRule{
{Source: "api_key", TargetField: "{env}-api-key"},
},
},
)
_ = sources.Execute(pls, values.New(),
sources.FromFile("config.yaml", middlewares.WithConfigMapper(mapper)),
)
Use a custom function when you need full control: conditional logic, array handling, value transformations, or cross-field validation thatβs not practical to express with patterns. The function receives the unmarshaled config as interface{} and returns a standard map[sectionSlug]map[paramName]any for Glazed to apply.
Provide a ConfigFileMapper function to WithConfigFileMapper to transform raw config into a map[sectionSlug]map[paramName]any:
package main
import (
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
)
var mapper middlewares.ConfigFileMapper = func(raw interface{}) (map[string]map[string]interface{}, error) {
// inspect raw (unmarshaled YAML/JSON) and build the section map
return map[string]map[string]interface{}{
"demo": {"api-key": "secret", "threshold": 5},
}, nil
}
_ = sources.Execute(pls, values.New(),
sources.FromFile("config.yaml", middlewares.WithConfigFileMapper(mapper)),
)
--print-parsed-fields)Parsing is not a black boxβevery write records its source and any relevant metadata. Enable --print-parsed-fields to see the exact sequence of updates for each field. This is invaluable when debugging precedence issues (for example, βwhy didnβt my local override win?β) or auditing where a value originated.
Add the command-settings section (done automatically by the Cobra parser unless disabled) and run with --print-parsed-fields to see where a value came from:
demo:
api-key:
log:
- source: config
metadata: { config_file: base.yaml, index: 0 }
value: base
- source: config
metadata: { config_file: local.yaml, index: 1 }
value: local
value: local
Validate configs early to catch mistakes before runtime. For default-shaped files, check for unknown sections/fields and type errors. For pattern-based configs, instantiate a mapper and call Map in a validate-only pass; the mapper will fail fast on missing required matches, ambiguous patterns, or invalid targets. These validators are small enough to run in CI and provide crisp error messages for contributors.
You can validate config files before applying them.
Apply this validator to YAML/JSON files that mirror your sections. Itβs conservative by design: any unexpected section or field is flagged, and values are type-checked against your field definitions. This keeps configs tidy and prevents silent drift as fields evolve.
package main
import (
"fmt"
"os"
"strings"
"github.com/go-go-golems/glazed/pkg/cmds/schema"
"github.com/go-go-golems/glazed/pkg/cmds/fields"
"gopkg.in/yaml.v3"
)
func validateConfigFile(schema_ *schema.Schema, path string) error {
b, err := os.ReadFile(path)
if err != nil { return err }
var raw map[string]interface{}
if err := yaml.Unmarshal(b, &raw); err != nil { return err }
issues := []string{}
for sectionSlug, v := range raw {
section, ok := schema_.Get(sectionSlug)
if !ok { issues = append(issues, fmt.Sprintf("unknown section %s", sectionSlug)); continue }
kv, ok := v.(map[string]interface{})
if !ok { issues = append(issues, fmt.Sprintf("section %s must be an object", sectionSlug)); continue }
pds := section.GetDefinitions()
known := map[string]bool{}
pds.ForEach(func(pd *fields.Definition) { known[pd.Name] = true })
for key, val := range kv {
if !known[key] { issues = append(issues, fmt.Sprintf("unknown field %s.%s", sectionSlug, key)); continue }
pd, _ := pds.Get(key)
if _, err := pd.CheckValueValidity(val); err != nil {
issues = append(issues, fmt.Sprintf("invalid value for %s.%s: %v", sectionSlug, key, err))
}
}
}
if len(issues) > 0 { return fmt.Errorf(strings.Join(issues, "\n")) }
return nil
}
Validate overlays per-file or implement overlay-aware required semantics if needed.
mapper.Map)For declarative mappings, the mapper is your validator. Build it once per app (construction validates static aspects) and call Map on the raw config (runtime semantics validate dynamic aspects). Error messages include path hints and prefix-aware field names to accelerate debugging.
package main
import (
"os"
"github.com/go-go-golems/glazed/pkg/cmds/schema"
pm "github.com/go-go-golems/glazed/pkg/cmds/middlewares/patternmapper"
"gopkg.in/yaml.v3"
)
rules, _ := pm.LoadRulesFromFile("mappings.yaml")
mapper, _ := pm.NewConfigMapper(pls, rules...)
b, _ := os.ReadFile("config.yaml")
var raw map[string]interface{}
_ = yaml.Unmarshal(b, &raw)
if _, err := mapper.Map(raw); err != nil {
// invalid config (required missing, collisions, unknown target params, ...)
}
These guidelines help keep your configuration predictable across environments and easy to reason about during reviews and incident response.
base.yaml, env.yaml, local.yaml.AppName in CobraParserConfig to enable env overrides automatically on the built-in parser path.sources.WithSource("config") (done for you by the config middlewares).Use these as templates. Each example shows a minimal, focused scenario you can copy-paste and expand within your own app. The validation script runs βhappy pathβ and negative tests to demonstrate failure modes and ensure your validators stay effective over time.
cmd/examples/config-singlecmd/examples/config-overlaycmd/examples/config-pattern-mappercmd/examples/config-custom-mapperglazed/ttmp/2025-11-03/validate-config-examples.shcmd/examples/config-planIf youβre migrating from Viper-based setups, replace per-command file injection and env parsing with Glazed middlewares and CobraParserConfig. This typically reduces glue code while improving observability (traceable parse steps) and testability (deterministic precedence).
Older Viper-based config parsing helpers have been removed. Prefer config middlewares (FromFile / FromFiles / FromResolvedFiles / FromConfigPlan) together with env updates and --config-file.