configschema

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 10 Imported by: 0

README

config-schema

JSON Schema validation for config — composable partial schemas, attributed results

Go Reference Pipeline phpboyscout Go toolkit

Part of the phpboyscout Go toolkit. Documented with the parent module at config.go.phpboyscout.uk


Several components each declare the configuration they need. The Store validates the merged result against all of them at once, and every failure names the component that raised it.

cache, _ := configschema.FromJSON("cache-plugin", cacheDoc)
server, _ := configschema.FromStruct("server-component", ServerConfig{})

store, err := config.NewStore(ctx,
    config.WithFiles(fsys, "app.yaml"),
    config.WithEnv("MYAPP"),
    config.WithSchemaAt("plugins.cache", cache),
    config.WithSchemaAt("server", server, config.Required),
)

for _, e := range store.Validate().Errors {
    fmt.Printf("%s: %s [%s]\n", e.Key, e.Message, e.Contributor)
}
// plugins.cache.ttl: minimum: got 0, want 1 [cache-plugin]

Why this is a separate module

config defines what validating a configuration means; this supplies the dialect, the library and the ingestion.

That split is not tidiness. Twenty-five adapter modules depend on config and each pins its dependency footprint — a JSON Schema library linked into the core would widen every one of them for a capability most do not use. Behind an interface it costs them nothing, and a consumer who wants validation takes this module and its two extra dependencies.

A schema is written in its own terms

A component declares enabled and ttl, not plugins.cache.enabled. The assembling code decides where it lives, so the same schema can be mounted twice and a component never hard-codes its own location.

Failures still report the full path, because a relative key is not one a user could edit.

An absent section is silent unless you say otherwise

A schema constrains a key only when the key is present, so a component whose whole section is missing says nothing. That is right for a plugin nobody enabled and wrong for a mandatory one, so the mount decides:

config.WithSchemaAt("server", server, config.Required)

Values are coerced to their schema type

An environment variable arrives as the string "8080", and a strict "type": "integer" would reject it — while GetInt("server.port") parses it happily. A validator contradicting the accessor a caller is about to use is worse than the mismatch, so a value is normalised to its declared type first.

This is a deliberate divergence from a stock validator: a document accepted here may be rejected by one run over the same JSON elsewhere. Coercion is not permissiveness — a string that is not a number stays a string and still fails.

Struct tags still work

type Config struct {
    Host string `config:"server.host" validate:"required"`
    Mode string `config:"log.mode" enum:"json,text"`
}

schema, err := configschema.FromStruct("app", Config{})

Dotted paths become nested schema. No off-the-shelf reflector does this — invopop/jsonschema and friends reflect Go structure, while these tags encode path, and the nesting a dotted path implies is not present in the type.

What it costs

Modules added 2santhosh-tekuri/jsonschema/v6 and golang.org/x/text
Requires config v0.14.0+, the release adding the config.Schema seam
Drafts 4, 6, 7, 2019-09 and 2020-12

Install

go get gitlab.com/phpboyscout/go/config-schema

Documentation

Licence

MIT — see LICENSE.

Documentation

Overview

Package configschema validates configuration against JSON Schema, as one implementation of config.Schema.

The core defines what validating a configuration means and takes no position on how a schema is expressed; this module supplies the dialect, the library and the ingestion. That split is what keeps a JSON Schema dependency out of the twenty-five adapter modules that depend on config and do not want one — see the composable schema validation spec, D7.

Several components each declare the configuration they need, mounted where the assembling code decides, and the Store validates the merged result against all of them at once:

config.NewStore(ctx,
    config.WithFiles(fsys, "app.yaml"),
    config.WithSchemaAt("plugins.cache", cacheSchema),
    config.WithSchemaAt("server", serverSchema, config.Required),
)

Every failure names the document that raised it, because a report aggregating several components is not actionable if it cannot say whose expectation was violated.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Schema

type Schema struct {
	// contains filtered or unexported fields
}

Schema is a compiled JSON Schema document that satisfies config.Schema.

func FromJSON

func FromJSON(name string, doc []byte) (*Schema, error)

FromJSON compiles a JSON Schema document.

name identifies the contributor in failure reports — use the component's name rather than a filename, because it is what a reader has to act on.

Compilation happens here rather than at validation time so a malformed document fails once, at wiring, instead of on every load.

func FromStruct

func FromStruct(name string, v any) (*Schema, error)

FromStruct derives a JSON Schema from the `config:` tags this estate already uses to declare configuration.

It exists because no off-the-shelf reflector can do this job. Checked against invopop/jsonschema v0.14.0: it reflects Go **structure**, and these tags encode **path**. Given

Host string `config:"server.host"`

on a flat struct, a reflector emits a flat property named for the Go field and ignores the tag entirely. The nesting a dotted path implies is not present in the type, so nothing can infer it — expanding it has to be done here.

Recognised tags, matching what config's own tag schema reads:

config:"a.b.c"          the dotted path, and the only required tag
validate:"required"     marks the key required at its own level
enum:"x,y,z"            restricts the value
description:"..."       carried into the schema

A field with no `config:` tag is skipped, and one tagged `-` is excluded.

func (*Schema) Name

func (s *Schema) Name() string

Name reports the contributor name failures are attributed to.

func (*Schema) Validate

func (s *Schema) Validate(snap *config.Snapshot, at string, r *config.ValidationResult)

Validate implements config.Schema.

at is where this document was mounted. The schema is written in its own terms — "enabled", "ttl" — and failures are reported at the full path, because a relative key is not one a user could edit.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL