Schema Validation¶
Advanced Features¶
Schema Validation¶
The Container supports decentralised, per-package schema validation using struct tags. Each package defines a struct describing the config keys it consumes and validates its own slice of the config — there is no centralised schema for the entire config tree.
This design aligns with GTB's config architecture where defaults live in embedded assets and each feature package owns its config independently.
Defaults vs Validation
Default values belong in your package's embedded assets/init/config.yaml, not in struct tags. The default tag is retained for documentation and error hints only — the validation layer does not inject defaults.
Struct tag reference:
| Tag | Purpose | Example |
|---|---|---|
config:"key" |
Maps field to config key | config:"github.token" |
validate:"required" |
Field must be present and non-zero | validate:"required" |
enum:"a,b,c" |
Restricts to allowed values | enum:"debug,info,warn,error" |
default:"value" |
Documentation only (used in hints) | default:"info" |
Per-package validation (recommended pattern):
Each package defines a config struct and validates the keys it owns with the
generic ValidateStruct[T] helper, which derives the schema from the struct's
tags and runs it against the container:
// pkg/myfeature/config.go
type Config struct {
APIKey string `config:"myfeature.api_key" validate:"required"`
Endpoint string `config:"myfeature.endpoint" validate:"required"`
LogLevel string `config:"myfeature.log_level" enum:"debug,info,warn,error" default:"info"`
}
func ValidateConfig(cfg config.Containable) error {
return config.ValidateStruct[Config](cfg)
}
ValidateStruct[T] takes the Containable interface, so there is no need to
type-assert Props.Config down to the concrete *Container. Schema options pass
straight through, e.g. config.ValidateStruct[Config](cfg, config.WithStrictMode()).
When you need the *Schema itself (for hot-reload gating via SetSchema, say),
build it with config.SchemaOf[Config](), which caches the schema per type.
Load-time validation (for CLI tools):
schema, err := config.NewSchema(config.WithStructSchema(AppConfig{}))
if err != nil {
return err
}
cfg, err := config.LoadFilesContainerWithSchema(fs, schema,
config.WithLogger(l),
config.WithConfigFiles("config.yaml"),
)
if err != nil {
// "config validation failed:
// myfeature.api_key: required field is missing (hint: ... set the MYFEATURE_API_KEY environment variable)"
return err
}
Validation on an existing container:
container := config.NewFilesContainer(fs,
config.WithLogger(l),
config.WithConfigFiles("config.yaml"),
)
result := container.Validate(schema)
if !result.Valid() {
fmt.Println(result.Error())
}
// Warnings (unknown keys) are available via result.Warnings
Schema options:
| Option | Description |
|---|---|
WithStructSchema(v any) |
Derive schema from struct tags |
WithStrictMode() |
Treat unknown keys as errors (default: warnings) |
Generic helpers:
| Function | Description |
|---|---|
SchemaOf[T](opts ...SchemaOption) |
Build a schema from T's struct tags; caches the option-free result per type |
ValidateStruct[T](cfg Containable, opts ...SchemaOption) |
Validate cfg against T's schema without a manual *Container cast |
Hot-reload integration: Attach a schema to a container via container.SetSchema(schema). When config files change, validation runs before notifying observers. Invalid reloads are rejected and logged.
For a complete walkthrough of defining config defaults AND validation for a new component, see the Validate Component Config how-to guide.