Skip to content

Repository files navigation

valgen

GitHub release (latest SemVer) Lint & Test Go Report Card GoDoc License

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 run go 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).

Install

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/valgen

Your 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).

Getting started

1. Standard validators

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@latest

To 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/valgen

Put 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.

2. Custom validators — a thin generator main

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/valgen

For 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.

Configuration

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.

Tag DSL syntax

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 Syntax vocabularyEscape, Sigil, And, Or, Quote, settable via .Syntax(...) (plugin.Syntax). The engine only acts on Escape (when splitting); Sigil/And/Or/Quote are opaque strings it threads onto every Context for 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.

Writing a validator

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.

Changelog

Notable changes are recorded in CHANGELOG.md.

Supported Go versions

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.

AI Policy

Use AI tools or not — either way, you own what you submit. See AI_POLICY.md.

License

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.

About

Code generation validation library engine

Resources

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages