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.
In order to add the library to your project, run:
go get github.com/nobl9/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/nobl9/govy/pkg/govy"
"github.com/nobl9/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+`),
"5 M. Skłodowska-Curie Square, 60-965 Poznan").
WithDetails("Polish address format must consist of the main address and zip code")),
)
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
}
- Nobl9 Go SDK.
This is wheregovy
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.
- go-playground/validator.
Visit runnable example for a comprehensive, live code comparison betweengovy
andgo-playground/validator
.validator
was the predecessor whichgovy
dethroned at Nobl9. For more trivia and details on the differences between the two, check out the rationale section.
Govy validation flow consists of the following building blocks:
Validator
is the top-level entity which usually aggregatesPropertyRules
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.
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.
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.
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.
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.
Govy comes with a set of predefined rules defined in the rules package which covers most of the common use cases.
If the predefined rules are not enough, you can easily define your own rules:
package examples
import (
"fmt"
"github.com/nobl9/govy/pkg/govy"
"github.com/nobl9/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
}
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/nobl9/govy/pkg/govy"
"github.com/nobl9/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+`),
"5 M. Skłodowska-Curie Square, 60-965 Poznan").
WithDetails("Polish address format must consist of the main address and zip code")).
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",
// "type": "string",
// "isOptional": true,
// "rules": [
// {
// "description": "each word in a string must start with a capital letter",
// "errorCode": "string_title"
// }
// ]
// },
// {
// "path": "$.name",
// "type": "string",
// "rules": [
// {
// "description": "string cannot be empty",
// "errorCode": "string_not_empty"
// },
// {
// "description": "must be one of: Jake, George",
// "errorCode": "one_of"
// }
// ]
// },
// {
// "path": "$.students",
// "type": "[]Student",
// "package": "github.com/nobl9/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",
// "type": "string",
// "rules": [
// {
// "description": "length must be between 9 and 9",
// "errorCode": "string_length"
// }
// ]
// },
// {
// "path": "$.university.address",
// "type": "string",
// "rules": [
// {
// "description": "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')",
// "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"
// ]
// }
// ]
// },
// {
// "path": "$.university.name",
// "type": "string",
// "rules": [
// {
// "description": "",
// "conditions": [
// "Teacher name is John"
// ]
// }
// ]
// }
// ]
// }
}
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/nobl9/govy/pkg/govy"
"github.com/nobl9/govy/pkg/govyconfig"
"github.com/nobl9/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'
}
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.
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.
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.
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.
Checkout both contributing guidelines and development instructions.
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' history is collected and can be viewed as charts over time here.
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.