valgen is a compile-time reimagining of
go-playground/validator: instead of reflecting over structs
at runtime, it reads your validate:"..." tags at build time and generates a static
func (v *T) Validate(ctx context.Context) error per struct. The result is no reflection on the hot
path, no per-request allocation on success, and tag errors caught at build time rather than as
first-request panics.
This repository is the engine: the generator, the plugin contract, and the tiny runtime the generated
code imports. It knows nothing about any specific validation tag — the standard validators (required,
gt, dive, oneof, …) live in
github.com/go-playground/valgen-validations.
Just want to validate your structs? You're almost certainly looking for valgen-validations — tag your structs, add a small generator
main, and rungo generate. Its README has the Getting Started guide, the full tag catalog, and runnable usage examples. Read on here only to write your own validators, embed the engine, or understand the plugin contract — this README never duplicates the tag docs, by design (the engine knows nothing about tags).
valgen (the engine) is a library — you import it from a generator main, the small program that
generates the code, alongside a validator set such as valgen-validations:
go get github.com/go-playground/valgenYour application imports only the tiny runtime packages the generated code references, never the engine. You
don't even need to write a generator main for the standard set — valgen-validations ships a default one you
run by version (below).
Point a go:generate directive at the default generator from valgen-validations — it runs the standard set
with defaults, fetched by version so nothing is added to your go.mod:
//go:generate go run github.com/go-playground/valgen-validations/cmd/valgen-validations@latestTo customize (your own validators, a custom error type, error-path namespacing, a different tag), write a
small generator main and point the directive at it instead:
// tools/valgen/main.go
package main
import (
"log"
validations "github.com/go-playground/valgen-validations/gen"
"github.com/go-playground/valgen/gen"
)
func main() {
// Generates the WHOLE module ("./...") by default, so cross-package nested
// structs are discovered and validated in one pass. (Narrow with .Patterns
// only if you know there are no cross-package references.)
if err := gen.New(validations.New().Generators()...).Run(); err != nil {
log.Fatal(err)
}
}//go:generate go run ./tools/valgenPut one directive at your module root and run go generate ./...; it writes a Validate(ctx) error per target struct. The generator main can live
in your module (as above) or — recommended for many repos — a separate module you run @version, which keeps
its build-time deps out of your app's go.mod. See the
valgen-validations Getting Started
and its tag catalog.
A generator is a separate program that parses your source. There is no global registry and no flag
parsing: you build the EXPLICIT set of TagGenerators with the fluent gen.Generator
and finish with Run, so nothing a dependency's init() does can inject a generator into your output — the
trusted set is exactly what you list, and all configuration is type-safe Go.
package main
import (
"log"
validations "github.com/go-playground/valgen-validations/gen"
acme "github.com/acme/valgen-acme/gen"
"github.com/go-playground/valgen/gen"
)
func main() {
// The engine (gen.New) is the top-level driver; you feed it the generator sets
// you trust. validations.New()....Generators() yields one repo's set — append
// as many as you like, from anywhere, then run them all in one pass.
gens := validations.New().Generators()
gens = append(gens, acme.EvenGen{}, acme.ColorGen{}) // your trusted extras, explicit
err := gen.New(gens...).
NameTag("json").
Mode(gen.CollectAll).
Run() // whole module ("./...") by default
if err != nil {
log.Fatal(err)
}
}//go:generate go run ./tools/valgenFor a complete, runnable version of this file, see
example/tools/valgen/main.go
in valgen-validations (it enables namespacing; drop .Namespace(true) for the default).
The builder replaces the old CLI flags with type-safe methods — each returns the builder and the chain ends
in the single Run() error terminal (every option is listed under Configuration below).
The engine stays independent of any validator package: each repo hands you a []TagGenerator (its
Generators()) that you compose into gen.New(...), so validators from several repos combine in one run
without any of them depending on each other. Within the set the last generator for a tag wins, so Add an
override after what it replaces. Targets are auto-detected: any validate-tagged struct, plus the structs
it reaches. See SECURITY.md.
gen.New(gens...) builds the generator; each method below returns the builder for chaining, and Run() is
the single terminal. All configuration is type-safe Go — the engine builder has no CLI flags. (A ready-made
generator such as valgen-validations' default command
may expose these non-code settings as its own flags, but the engine API itself stays code-only.)
| Method | Default | Controls |
|---|---|---|
New(gens...) |
— | Construct the generator with the EXPLICIT trusted set (no global registry; last generator for a tag wins) |
Add(gens...) |
— | Append more TagGenerators to the set (last-wins per tag) — layer a custom validator onto a standard set |
Dir(dir) |
"." |
Directory to load and generate in (go generate sets cwd to the directive's package) |
Patterns(...) |
./... (the whole module) |
Go package patterns to load. Whole-module is the default so cross-package nested validation is correct in one pass; narrow it (e.g. .) only when you know there are no cross-package references (a narrower scope silently skips recursion into structs outside it) |
NameTag(key) |
"" (Go field names) |
Struct-tag key for error display names (e.g. "json") |
ValidateTag(key) |
"validate" |
Struct-tag key holding the validation rules — configurable, not hard-coded |
Output(name) |
"valgen_gen.go" |
Generated file name (one per package) |
Mode(m) |
FailFast |
Per-field chain failure mode: FailFast stops at the first failing rule; CollectAll reports every failure |
Namespace(on) |
false (off) |
Error-path namespacing (User.Address.Street). Off means no ctx lookup, no context.WithValue per nested Validate, no path concat, and Violation.Namespace is "" |
Syntax(s) |
DefaultSyntax() |
Tag DSL lexical vocabulary (see Tag DSL syntax) — a zero value is completed with defaults |
Run() |
— | Terminal: execute generation. Positioned build failures are combined into one error and nothing is written |
The generated method is always named Validate (func (v *T) Validate(ctx context.Context) error).
This name is fixed by design, not configurable: a struct auto-recurses into nested structs and calls
cross-package targets by that known name and signature, so it must be stable everywhere.
The engine is tag-agnostic — it knows nothing about gt, dive, oneof, and never interprets a tag's
meaning — but it does own the lexical grammar of the validate:"..." mini-language: how a tag string is
split into rules and params, and the vocabulary (sigil, combinators, quote, escape) that validators build
on. Two parts:
- Fixed engine grammar — the rule (
,) and tag/param (=) separators are constants; validators never redefine them. - Configurable
Syntaxvocabulary —Escape,Sigil,And,Or,Quote, settable via.Syntax(...)(plugin.Syntax). The engine only acts onEscape(when splitting);Sigil/And/Or/Quoteare opaque strings it threads onto everyContextfor validators to interpret.
| Element | Token | Configurable | Meaning |
|---|---|---|---|
| Rule separator | , |
No (fixed grammar) | Separates rules in a chain (gt=0,lte=10) — an AND sequence run in order |
| Tag/param separator | = |
No (fixed grammar) | Splits a rule's tag from its param at the first = (gt=10) |
| Escape | \ |
Syntax.Escape |
Forces the next character literal (eq=a\,b → the literal a,b) — the only vocabulary the engine itself acts on |
| Sigil | @ |
Syntax.Sigil |
Field reference (eq=@Other) — interpreted by validators |
| Or | || |
Syntax.Or |
OR combinator for value/condition lists — interpreted by validators |
| And | && |
Syntax.And |
AND combinator for value/condition lists — interpreted by validators |
| Quote | ' |
Syntax.Quote |
Quote a value with spaces or an empty value (oneof='in progress' done) — interpreted by validators |
Because the engine splits rules on , and the tag/param on the first = before a validator ever sees
the param, a , or = that must appear inside a quoted value still needs the \ escape (eq=a\,b):
quoting covers spaces and empty values, escaping covers the engine-level separators.
A validator implements plugin.TagGenerator
(Tag() string + Generate(*plugin.Context) error) and emits the Go code for its check. It gets the real
go/types.Type plus the whole-program model, continues the rule chain with ctx.EmitRest(), and appends
its own error value via ctx.Fail(...). Export it (no self-registration) so a generator main can add it to
the explicit set:
gen.New(validations.New().Generators()...).Add(acme.GtGen{}).Run()The engine defines no error type — validators emit their own — and the generated file imports only the
runtime it needs. The generated method uses valgen.Errors
(an errors.Join accumulator). Error-path namespacing is off by default (.Namespace(true) opts in):
when on, the method threads a dotted path with
valgen.WithNamespace/valgen.Namespace
(one context.WithValue per nested Validate); when off, nested Validate gets the raw ctx — no
allocation, and error paths are empty. See the runnable examples.
Notable changes are recorded in CHANGELOG.md.
Aligned with the Go release policy, support is guaranteed for the two
most recent major versions of Go (the go directive in go.mod is the current MSGV — Minimum Supported Go
Version).
This does not mean the package won't work with older versions of Go, only that we reserve the right to raise the MSGV when needed to address security patches, OS support, or newly introduced functionality that materially benefits the package. Any MSGV increase ships in at least a minor release.
Use AI tools or not — either way, you own what you submit. See AI_POLICY.md.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this package by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.