govy
strongly-typed validation rules over functional interface, powered by generics and reflection free with heavy focus on crafting clear and information-rich error messages
About govy
This large README is shown as a plain-text preview. Read the full README at the source
# govy
[](https://pkg.go.dev/github.com/nobl9/govy)
[](https://raw.githack.com/wiki/nobl9/govy/coverage.html)
Validation library for Go that uses a functional interface for building
strongly-typed validation rules, powered by generics and
[reflection free](#reflection).
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](#validation-plan).
**GO** **V**alidate **Y**ourself!
**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](https://github.com/orgs/nobl9/projects/5/views/2?pane=info)
for upcoming, planned features.
## Legend
1. [Getting started](#getting-started)
1. [Agent skill](#agent-skill)
2. [Use cases](#use-cases)
3. [Comparison with other libraries](#comparison-with-other-libraries)
2. [Building blocks](#building-blocks)
1. [Errors](#errors)
3. [Features](#features)
1. [Type safety](#type-safety)
2. [Immutability](#immutability)
3. [Verbose error messages](#verbose-error-messages)
4. [Error message templates](#error-message-templates)
5. [Error message templates in custom rules](#error-message-templates-in-custom-rules)
6. [Predefined rules](#predefined-rules)
7. [Custom rules](#custom-rules)
8. [Validation plan](#validation-plan)
9. [Properties path inference](#properties-path-inference)
10. [Testing helpers](#testing-helpers)
4. [Rationale](#rationale)
1. [Reflection](#reflection)
2. [Trivia](#trivia)
5. [Development](#development)
1. [Tests coverage](#tests-coverage)
2. [Benchmarks](#benchmarks)
6. [Acknowledgments](#acknowledgments)
## Getting started
In order to add the library to your project, run:
```shell
go get github.com/nobl9/govy
```
There's an interactive tutorial available,
powered by Go's [testable examples](https://go.dev/blog/examples),
to access it visit [pkg.go.dev](https://pkg.go.dev/github.com/nobl9/govy)
or locally at [example_test.go](./pkg/govy/example_test.go).
Govy's code documentation is available at
[pkg.go.dev](https://pkg.go.dev/github.com/nobl9/govy).
You can read
[this blog post](https://www.nobl9.com/resources/type-safe-validation-in-go-with-govy)
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:
[//]: # (embed: internal/examples/readme_intro_example_test.go)
```go
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.]+, \d{2}-\d{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.]+, \d{2}-\d{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
}
```
### Agent skill
The [Govy skill](.agents/skills/govy/SKILL.md) helps coding agents write,
review, and test validators.
Install it in your project with the [skills CLI](https://skills.sh/docs/cli):
```shell
npx skills add nobl9/govy --skill govy
```
### Use cases
1. [Nobl9 Go SDK](https://github.com/nobl9/nobl9-go) \
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.
2. [OpenSLO](https://github.com/OpenSLO/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
1. [go-playground/validator](https://github.com/go-playground/validator) \
Visit [runnable example](./docs/validator-comparison/example_test.go) 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](#rationale) section.
## Building blocks
Govy validation flow consists of the following building blocks:

1. `Validator` is the top-level entity which usually
aggregates `PropertyRules` for a single struct.
2. `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.
3. `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](https://gist.github.com/nieomylnieja/268841b12571e8bb3a01780e8e6663c5),
the nil assertions on produced `error` would fail.
More details available in the
[laws of reflection blog post](https://go.dev/blog/laws-of-reflection#TOC_3.).
## Features
### Type safety
Govy is built on top of [Go's generics](https://go.dev/doc/tutorial/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`.
They follow a
[JSONPath](https://datatracker.ietf.org/doc/html/rfc9535)-compatible syntax,
but error messages omit the leading `$` root segment for better human readability.
```text
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](https://pkg.go.dev/text/template).
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).
[//]: # (embed: internal/examples/readme_message_templates_example_test.go)
```go
package examples
import (
"fmt"
"github.com/nobl9/govy/pkg/govy"
"github.com/nobl9/govy/pkg/rules"
)
func Example_messageTemplates() {
type Teacher struct {
Name string `json:"name"`
}
templateString := "name length must 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 must 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`.
[//]: # (embed: internal/examples/readme_adding_message_templates_support_example_test.go)
```go
package examples
import (
"fmt"
"github.com/nobl9/govy/pkg/govy"
"github.com/nobl9/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"),
),
).WithNameFunc(govy.NameFuncFromTypeName[Teacher]())
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](./pkg/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:
[//]: # (embed: internal/examples/readme_custom_rule_example_test.go)
```go
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"),
),
).WithNameFunc(govy.NameFuncFromTypeName[Teacher]())
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.
[//]: # (embed: internal/examples/readme_validation_plan_example_test.go)
```go
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").
Rules(rules.StringMatchRegexp(
regexp.MustCompile(`[\w\s.]+, \d{2}-\d{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").
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, err := govy.Plan(teacherValidator, govy.PlanStrictMode())
if err != nil {
panic(err)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(plan)
// Output:
// {
// "name": "Teacher",
// "typeInfo": {
// "name": "Teacher",
// "kind": "struct",
// "package": "github.com/nobl9/govy/internal/examples"
// },
// "properties": [
// {
// "path": "$.middleName",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "rules": [
// {
// "description": "property is optional",
// "errorCode": "optional"
// },
// {
// "description": "each word in a string must start with a capital letter",
// "errorCode": "string_title"
// }
// ]
// },
// {
// "path": "$.name",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "values": [
// "Jake",
// "George"
// ],
// "rules": [
// {
// "description": "string must not 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/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",
// "typeInfo": {
// "name": "string",
// "kind": "string"
// },
// "rules": [
// {Frequently Asked Questions
What is govy?
govy is a Validation library for the Go programming language. strongly-typed validation rules over functional interface, powered by generics and reflection free with heavy focus on crafting clear and information-rich error messages
How do I install govy?
Install govy with the Go module system using `go get nobl9/govy`. Check the repository for the current installation instructions.
What category does govy belong to?
govy is listed under Validation, specifically Validation.