govy

Validation library for Go that uses a functional interface for building
strongly-typed validation rules, powered by generics and
reflection free.
It puts heavy focus on end user errors readability,
providing means of crafting clear and information-rich error messages.
It also allows writing self-documenting validation rules through a
validation plan.
GO Validate Yourself!
DISCLAIMER: govy is in active development, while the core API is unlikely
to change, breaking changes may be introduced with new versions until v1
is released.
Checkout roadmap
for upcoming, planned features.
Legend
- Getting started
- Use cases
- Comparison with other libraries
- Building blocks
- Errors
- Features
- Type safety
- Immutability
- Verbose error messages
- Error message templates
- Error message templates in custom rules
- Predefined rules
- Custom rules
- Validation plan
- Properties name inference
- Testing helpers
- Rationale
- Reflection
- Trivia
- Development
- Tests coverage
- Benchmarks
- Acknowledgments
Getting started
In order to add the library to your project, run:
go get github.com/basicbrown/govy
There's an interactive tutorial available,
powered by Go's testable examples,
to access it visit pkg.go.dev
or locally at example_test.go.
Govy's code documentation is available at
pkg.go.dev.
You can read
this blog post
for a quick overview of the library, its capabilities,
what distinguishes it from other solutions and why was it conceived.
Here's a quick example of govy in action:
package examples
import (
"fmt"
"regexp"
"time"
"github.com/basicbrown/govy/pkg/govy"
"github.com/basicbrown/govy/pkg/rules"
)
func Example_basicUsage() {
type University struct {
Name string `json:"name"`
Address string `json:"address"`
}
type Student struct {
Index string `json:"index"`
}
type Teacher struct {
Name string `json:"name"`
Age time.Duration `json:"age"`
Students []Student `json:"students"`
MiddleName *string `json:"middleName,omitempty"`
University University `json:"university"`
}
universityValidation := govy.New(
govy.For(func(u University) string { return u.Name }).
WithName("name").
Required(),
govy.For(func(u University) string { return u.Address }).
WithName("address").
Required().
Rules(rules.StringMatchRegexp(
regexp.MustCompile(`[\w\s.]+, [0-9]{2}-[0-9]{3} \w+`),
).
WithDetails("Polish address format must consist of the main address and zip code").
WithExamples("5 M. Skłodowska-Curie Square, 60-965 Poznan")),
)
studentValidator := govy.New(
govy.For(func(s Student) string { return s.Index }).
WithName("index").
Rules(rules.StringLength(9, 9)),
)
teacherValidator := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
WithName("name").
Required().
Rules(
rules.StringNotEmpty(),
rules.OneOf("Jake", "George")),
govy.ForPointer(func(t Teacher) *string { return t.MiddleName }).
WithName("middleName").
Rules(rules.StringTitle()),
govy.ForSlice(func(t Teacher) []Student { return t.Students }).
WithName("students").
Rules(
rules.SliceMaxLength[[]Student](2),
rules.SliceUnique(func(v Student) string { return v.Index })).
IncludeForEach(studentValidator),
govy.For(func(t Teacher) University { return t.University }).
WithName("university").
Include(universityValidation),
).
When(func(t Teacher) bool { return t.Age < 50 })
teacher := Teacher{
Name: "John",
MiddleName: nil, // Validation for nil pointers by default is skipped.
Age: 48,
Students: []Student{
{Index: "918230014"},
{Index: "9182300123"},
{Index: "918230014"},
},
University: University{
Name: "",
Address: "10th University St.",
},
}
if err := teacherValidator.WithName("John").Validate(teacher); err != nil {
fmt.Println(err)
}
// When condition is not met, no validation errors.
johnFromTheFuture := teacher
johnFromTheFuture.Age = 51
if err := teacherValidator.WithName("John From The Future").Validate(johnFromTheFuture); err != nil {
fmt.Println(err)
}
// Output:
// Validation for John has failed for the following properties:
// - 'name' with value 'John':
// - must be one of: Jake, George
// - 'students' with value '[{"index":"918230014"},{"index":"9182300123"},{"index":"918230014"}]':
// - length must be less than or equal to 2
// - elements are not unique, 1st and 3rd elements collide
// - 'students[1].index' with value '9182300123':
// - length must be between 9 and 9
// - 'university.name':
// - property is required but was empty
// - 'university.address' with value '10th University St.':
// - string must match regular expression: '[\w\s.]+, [0-9]{2}-[0-9]{3} \w+' (e.g. '5 M. Skłodowska-Curie Square, 60-965 Poznan'); Polish address format must consist of the main address and zip code
}
Use cases
- Nobl9 Go SDK.
This is where govy was born,
it's used for validating complex k8s-like schema, it contains both simple
and very advanced validation rules and is a great place to draw some
inspiration from.
- OpenSLO.
It's used for validating open specification for defining SLOs.
The specification is a complex, YAML-based and k8s compatible schema,
similar to Nobl9's configuration.
Comparison with other libraries
- go-playground/validator.
Visit runnable example for a
comprehensive, live code comparison between govy and
go-playground/validator.
validator was the predecessor which govy dethroned at Nobl9.
For more trivia and details on the differences between the two,
check out the rationale section.
Building blocks
Govy validation flow consists of the following building blocks:

Validator is the top-level entity which usually
aggregates PropertyRules for a single struct.
PropertyRules is a representation of a single property's validation rules.
It usually represents a single struct field.
It comes with two extra variants specifically designed for slices and maps.
These allow defining rules for each element, key or value of the property.
Rule defines a single validation rule.
Multiple rules can be combined to form a more complex
validation rule using RuleSet.
Errors
Govy errors are structured (as in each is a struct) and reflect the
aforementioned building blocks hierarchy:

The exception being PropertyErrors which is an additional container for
grouping PropertyError without the context of a specific Validator.
Govy functions return error interface.
In order to access the underlying structured error, you need to type cast it.
The reason for that is the interface type implementation in Go.
If, hypothetically, Validator would return *ValidatorError directly,
and given the following code in this
GitHub gist,
the nil assertions on produced error would fail.
More details available in the
laws of reflection blog post.
Features
Type safety
Govy is built on top of Go's generics.
Thanks to that it is able to provide a robust and extensible API which is still
type-safe.
Immutability
Govy components are largely immutable and lazily loaded:
- Immutable, as changing the pipeline through chained functions,
will return a new pipeline.
It allows extended reusability of validation components.
- Lazily loaded, as properties are extracted through getter functions,
which are only called when you call the
Validate method.
Functional approach allows validation components to only be called when
needed.
You should define your pipeline once and call it
whenever you validate instances of your entity.
Verbose error messages
Default govy error messages are verbose and provide a clear indication both
of the error cause and the property context in which they occurred.
The property paths are evaluated relative to the root Validator and follow
JSON path standard.
Validation for Teacher has failed for the following properties:
- 'name' with value 'John':
- must be one of [Jake, George]
- 'students' with value '[{"index":"918230014"},{"index":"9182300123"},{"index":"918230014"}]':
- length must be less than or equal to 2
- elements are not unique, index 0 collides with index 2
- 'students[1].index' with value '9182300123':
- length must be between 9 and 9
- 'university.address':
- property is required but was empty
The errors themselves are structured and can be parsed programmatically
allowing custom error handling.
They come with exported fields, JSON tags and can be easily serialized and
deserialized.
Error message templates
If you want a more fine-grained control over the error messages,
you can define custom error message templates for each builtin rule.
The templates are powered by Go's native templating system.
Each builtin validation rule has specific variables available
and there are also builtin functions shipped which help construct
the message templates (like formatExamples in the example below).
package examples
import (
"fmt"
"github.com/basicbrown/govy/pkg/govy"
"github.com/basicbrown/govy/pkg/rules"
)
func Example_messageTemplates() {
type Teacher struct {
Name string `json:"name"`
}
templateString := "name length should be between {{ .MinLength }} and {{ .MaxLength }} {{ formatExamples .Examples }}"
v := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
WithName("name").
Rules(
rules.StringLength(5, 10).
WithExamples("Joanna", "Jerry").
WithMessageTemplateString(templateString),
),
).WithName("Teacher")
teacher := Teacher{Name: "Tom"}
err := v.Validate(teacher)
if err != nil {
fmt.Println(err)
}
// Output:
// Validation for Teacher has failed for the following properties:
// - 'name' with value 'Tom':
// - name length should be between 5 and 10 (e.g. 'Joanna', 'Jerry')
}
Error message templates in custom rules
If you wish to support templating in your custom rules, you need to make sure
your rules ALWAYS return govy.RuleErrorTemplate and supply the template
with either govy.Rule.WithMessageTemplateString or govy.Rule.WithMessageTemplate.
package examples
import (
"fmt"
"github.com/basicbrown/govy/pkg/govy"
"github.com/basicbrown/govy/pkg/rules"
)
func Example_addingMessageTemplatesSupportToCustomRules() {
type Teacher struct {
Name string `json:"name"`
}
template := `{{ .PropertyValue }} must be {{ .ComparisonValue }}; {{ .Custom.Foo }} and {{ .Custom.Baz }}`
customRule := govy.NewRule(func(name string) error {
if name != "John" {
return govy.NewRuleErrorTemplate(govy.TemplateVars{
PropertyValue: name,
ComparisonValue: "John",
Custom: map[string]any{
"Foo": "Bar",
"Baz": 42,
},
})
}
return nil
}).
WithErrorCode("custom_rule").
WithMessageTemplateString(template).
WithDetails("we just don't like anyone but Johns...").
WithDescription("must be John")
teacherValidator := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
WithName("name").
Required().
Rules(
customRule,
rules.StringStartsWith("J"),
),
).InferName()
teacher := Teacher{Name: "George"}
if err := teacherValidator.Validate(teacher); err != nil {
fmt.Println(err)
}
// Output:
// Validation for Teacher has failed for the following properties:
// - 'name' with value 'George':
// - George must be John; Bar and 42
// - string must start with 'J' prefix
}
Predefined rules
Govy comes with a set of predefined rules defined
in the rules package which covers most of the common use cases.
Custom rules
If the predefined rules are not enough, you can easily define your own rules:
package examples
import (
"fmt"
"github.com/basicbrown/govy/pkg/govy"
"github.com/basicbrown/govy/pkg/rules"
)
func Example_customRules() {
type Teacher struct {
Name string `json:"name"`
}
customRule := govy.NewRule(func(name string) error {
if name != "John" {
return fmt.Errorf("must be John")
}
return nil
}).
WithErrorCode("custom_rule").
WithDetails("we just don't like anyone but Johns...").
WithDescription("must be John")
teacherValidator := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
WithName("name").
Required().
Rules(
customRule,
rules.StringStartsWith("J")),
).InferName()
teacher := Teacher{Name: "George"}
if err := teacherValidator.Validate(teacher); err != nil {
fmt.Println(err)
}
// Output:
// Validation for Teacher has failed for the following properties:
// - 'name' with value 'George':
// - must be John; we just don't like anyone but Johns...
// - string must start with 'J' prefix
}
Validation plan
DISCLAIMER: This feature is experimental and is subject to change.
Validation plan provides a way to self-document your validation rules.
It helps keep your documentation and validation rules in sync.
It produces a structured output which can be handled programmatically
or directly encoded to JSON.
package examples
import (
"encoding/json"
"os"
"regexp"
"time"
"github.com/basicbrown/govy/pkg/govy"
"github.com/basicbrown/govy/pkg/rules"
)
func Example_validationPlan() {
type University struct {
Name string `json:"name"`
Address string `json:"address"`
}
type Student struct {
Index string `json:"index"`
}
type Teacher struct {
Name string `json:"name"`
Age time.Duration `json:"age"`
Students []Student `json:"students"`
MiddleName *string `json:"middleName,omitempty"`
University University `json:"university"`
}
universityValidation := govy.New(
govy.For(func(u University) string { return u.Name }).
WithName("name").
Required(),
govy.For(func(u University) string { return u.Address }).
WithName("address").
Required().
Rules(rules.StringMatchRegexp(
regexp.MustCompile(`[\w\s.]+, [0-9]{2}-[0-9]{3} \w+`),
).
WithDetails("Polish address format must consist of the main address and zip code").
WithExamples("5 M. Skłodowska-Curie Square, 60-965 Poznan")).
When(func(u University) bool { return u.Name == "PUT" },
govy.WhenDescription("University name is PUT University")),
)
studentValidator := govy.New(
govy.For(func(s Student) string { return s.Index }).
WithName("index").
Rules(rules.StringLength(9, 9)),
)
teacherValidator := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
WithName("name").
Required().
Rules(
rules.StringNotEmpty(),
rules.OneOf("Jake", "George")),
govy.ForPointer(func(t Teacher) *string { return t.MiddleName }).
WithName("middleName").
Rules(rules.StringTitle()),
govy.ForSlice(func(t Teacher) []Student { return t.Students }).
WithName("students").
Rules(
rules.SliceMaxLength[[]Student](2),
rules.SliceUnique(func(v Student) string { return v.Index })).
IncludeForEach(studentValidator),
govy.For(func(t Teacher) University { return t.University }).
WithName("university").
Include(universityValidation).
When(func(t Teacher) bool { return t.Name == "John" },
govy.WhenDescription("Teacher name is John")),
).
WithName("Teacher")
plan := govy.Plan(teacherValidator)
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(plan)
// Output:
// {
// "name": "Teacher",
// "properties": [
// {
// "path": "$.middleName",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "isOptional": true,
// "rules": [
// {
// "description": "each word in a string must start with a capital letter",
// "errorCode": "string_title"
// }
// ]
// },
// {
// "path": "$.name",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "rules": [
// {
// "description": "string cannot be empty",
// "errorCode": "string_not_empty"
// },
// {
// "description": "must be one of: Jake, George",
// "errorCode": "one_of"
// }
// ]
// },
// {
// "path": "$.students",
// "typeInfo": {
// "name": "[]Student",
// "kind": "[]struct",
// "package": "github.com/basicbrown/govy/internal/examples"
// },
// "rules": [
// {
// "description": "length must be less than or equal to 2",
// "errorCode": "slice_max_length"
// },
// {
// "description": "elements must be unique",
// "errorCode": "slice_unique"
// }
// ]
// },
// {
// "path": "$.students[*].index",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "rules": [
// {
// "description": "length must be between 9 and 9",
// "errorCode": "string_length"
// }
// ]
// },
// {
// "path": "$.university.address",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "rules": [
// {
// "description": "string must match regular expression: '[\\w\\s.]+, [0-9]{2}-[0-9]{3} \\w+'",
// "details": "Polish address format must consist of the main address and zip code",
// "errorCode": "string_match_regexp",
// "conditions": [
// "Teacher name is John",
// "University name is PUT University"
// ],
// "examples": [
// "5 M. Skłodowska-Curie Square, 60-965 Poznan"
// ]
// }
// ]
// },
// {
// "path": "$.university.name",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "rules": [
// {
// "description": "",
// "conditions": [
// "Teacher name is John"
// ]
// }
// ]
// }
// ]
// }
}
Properties name inference
DISCLAIMER: This feature is experimental and is subject to change.
Govy provides a way to automatically infer property names from the code itself.
This way, there's no need to manually provide properties' names with
WithName function.
Checkout example_test.go for an interactive
introduction to this feature.
Documentation for the name inference code generator is available
here.
package examples
import (
"fmt"
"github.com/basicbrown/govy/pkg/govy"
"github.com/basicbrown/govy/pkg/govyconfig"
"github.com/basicbrown/govy/pkg/rules"
)
func Example_nameInference() {
govyconfig.SetNameInferIncludeTestFiles(true) // Required for the example to run.
govyconfig.SetNameInferMode(govyconfig.NameInferModeRuntime)
defer govyconfig.SetNameInferIncludeTestFiles(false)
defer govyconfig.SetNameInferMode(govyconfig.NameInferModeDisable)
type Teacher struct {
Name string `json:"name"`
}
v := govy.New(
govy.For(func(t Teacher) string { return t.Name }).
Rules(rules.EQ("Jerry")),
).InferName()
teacher := Teacher{Name: "Tom"}
err := v.Validate(teacher)
if err != nil {
fmt.Println(err)
}
// Output:
// Validation for Teacher has failed for the following properties:
// - 'name' with value 'Tom':
// - should be equal to 'Jerry'
}
Testing helpers
Package govytest provides utilities which aid the process of
writing unit tests for validation rules defined with govy.
Checkout testable examples
for a concise overview of the package's capabilities.
Rationale
Why was this library created?
Most of the established Go libraries for validation were created
in a pre-generics era. They often use reflection in order to provide
generic validation API, it also allows them to utilize struct tags, which
further minimize the amount of code users need to write.
Unfortunately, the ease of use compromises Go's core language feature,
type safety and increases the complexity of the code.
Enter, generics.
With generics on board, it's finally possible to write a robust and type safe
validation API, thus govy was born.
Reflection
Is govy truly reflection free?
The short answer is yes, the long answer is govy does not utilize
reflection other than to serve better error messages or devise a
validation plan.
Some builtin rules might also use reflect, but the core functionality
does not rely on it.
Trivia
The library was first conceived at
nobl9-go,
which is Nobl9's Go SDK.
It was born out of a need for a better validation mechanism,
which would also allow us to auto-document validation rules.
At the time, we were using go-playground/validator,
while it's a great, matured library,
it is quiet "magical" as it operates entirely on reflection.
It's default errors are also not very informative.
Furthermore, our validation rules were quiet complex and figuring out which rules
were associated with given property was tedious to say the least.
Around the same time, Go 1.18 was released with generics support, we started playing
with them, and the idea for govy was born.
Development
Checkout both contributing guidelines and
development instructions.
Tests coverage
Tests coverage HTML for current main branch state can be inspected
here.
Note that cmd package is tested by building and running Go binaries directly.
This means there won't be any coverage for some of the core functions there.
Benchmarks
Benchmarks' history is collected and can be viewed as charts over time
here.
Acknowledgments
The API along with the accompanying nomenclature was heavily inspired by the
awesome Fluent Validation
library for C#.
Special thanks to go-playground/validator
for paving the way for Go validation libraries,
many predefined rules have been ported from it.
Handcrafted with ❤️ at Nobl9.