https://github.com/zoobz-io/check
Fluent validation for Go with struct tag verification
https://github.com/zoobz-io/check
fluent-api go golang struct-tags type-safety validation zoobzio
Last synced: 3 months ago
JSON representation
Fluent validation for Go with struct tag verification
- Host: GitHub
- URL: https://github.com/zoobz-io/check
- Owner: zoobz-io
- License: mit
- Created: 2026-01-22T02:36:46.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-03-20T02:56:19.000Z (4 months ago)
- Last Synced: 2026-03-20T05:23:17.555Z (4 months ago)
- Topics: fluent-api, go, golang, struct-tags, type-safety, validation, zoobzio
- Language: Go
- Homepage: https://check.zoobz.io
- Size: 78.1 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
README
# check
[](https://github.com/zoobz-io/check/actions/workflows/ci.yml)
[](https://codecov.io/gh/zoobz-io/check)
[](https://goreportcard.com/report/github.com/zoobz-io/check)
[](https://github.com/zoobz-io/check/security/code-scanning)
[](https://pkg.go.dev/github.com/zoobz-io/check)
[](LICENSE)
[](go.mod)
[](https://github.com/zoobz-io/check/releases)
Fluent validation for Go with struct tag verification.
## Usage
```go
type User struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Name *string `json:"name" validate:"omitempty,max=100"`
Age int `json:"age" validate:"min=13,max=120"`
}
func (u *User) Validate() error {
return check.Check[User](
check.Str(u.Email, "email").Required().Email().MaxLen(255).V(),
check.Str(u.Password, "password").Required().MinLen(8).V(),
check.OptStr(u.Name, "name").MaxLen(100).V(),
check.Num(u.Age, "age").Between(13, 120).V(),
).Err()
}
```
`Check[T]` validates your fields and verifies that every field with a `validate` tag was actually checked. Forget a field? You'll know:
```text
password: tagged but not validated (validate: required,min=8)
```
No magic, no reflection at validation time—just functions that return validation results.
```go
r := user.Validate()
if r.Err() != nil {
fmt.Println(r.Err())
// email: must be a valid email address; age: must be between 13 and 120
for _, fe := range check.GetFieldErrors(r) {
fmt.Printf("%s: %s\n", fe.Field, fe.Message)
}
// email: must be a valid email address
// age: must be between 13 and 120
}
```
Validation logic lives where you can see it, test it, and refactor it.
## Install
```bash
go get github.com/zoobz-io/check
```
Requires Go 1.24+.
## Fluent Builders
Chain validators naturally:
```go
// Strings
check.Str(email, "email").Required().Email().MaxLen(255).V()
// Optional strings (nil skips validation)
check.OptStr(name, "name").MaxLen(100).V()
// Numbers
check.Num(age, "age").Between(13, 120).V()
// Integers (adds Even, Odd, MultipleOf)
check.Int(count, "count").Positive().Even().V()
// Slices with auto-generated field names
check.StrSlice(tags, "tags").NotEmpty().MaxItems(10).Each(func(b *check.StrBuilder) {
b.MaxLen(50) // Validates tags[0], tags[1], etc.
}).V()
```
Conditional validation with `.When()`:
```go
check.Str(password, "password").
Required().
When(requireStrong, func(b *check.StrBuilder) {
b.MinLen(12).Match(complexityRegex)
}).V()
```
## Direct Functions
Use validators directly when you don't need the fluent API:
```go
check.All(
check.Required(email, "email"),
check.Email(email, "email"),
check.Between(age, 13, 120, "age"),
)
```
## Capabilities
| Category | Builders | Functions |
| ----------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Strings | `Str`, `OptStr` | `Required`, `MinLen`, `MaxLen`, `Match`, `Prefix`, `Suffix`, `OneOf`, `Alpha`, `Slug`, etc. |
| Numbers | `Num`, `OptNum`, `Int`, `OptInt` | `Min`, `Max`, `Between`, `Positive`, `Negative`, `NonZero`, `MultipleOf`, `Percentage` |
| Slices | `Slice`, `OptSlice`, `StrSlice`, `OptStrSlice` | `NotEmpty`, `MinItems`, `Unique`, `ContainsAll`, `Each`, `AllSatisfy`, `Subset` |
| Formats | (via `Str` methods) | `Email`, `URL`, `UUID`, `IP`, `CIDR`, `Semver`, `E164`, `CreditCard`, `JSON`, `Base64` |
| Comparison | — | `Equal`, `NotEqual`, `GreaterThan`, `LessThan`, `EqualField`, `GreaterThanField` |
| Maps | — | `NotEmptyMap`, `HasKey`, `HasKeys`, `OnlyKeys`, `EachKey`, `EachMapValue`, `UniqueValues` |
| Pointers | — | `NotNil`, `Nil`, `NilOr`, `RequiredPtr`, `DefaultOr`, `Deref` |
| Time | — | `Before`, `After`, `InPast`, `InFuture`, `BetweenTime`, `WithinDuration`, `NotWeekend` |
| Aggregation | — | `All` (collect all errors), `First` (fail-fast), `Merge`, `Check[T]` (with tag verification) |
## Validation Tracking
Check tracks which validators were applied to which fields, enabling downstream verification of validation coverage.
```go
r := check.All(
check.Required(email, "email"),
check.Email(email, "email"),
check.Between(age, 13, 120, "age"),
)
// Check if specific validators ran
r.HasValidator("email", "required") // true
r.HasValidator("email", "email") // true
r.HasValidator("age", "min") // true (Between reports min and max)
r.HasValidator("age", "max") // true
// Get all validators for a field
r.ValidatorsFor("email") // []string{"required", "email"}
// Get all validated fields
r.Fields() // []string{"email", "age"}
// Get full tracking map
r.Applied() // map[string][]string{"email": {"required", "email"}, "age": {"min", "max"}}
```
This enables tools to verify that declared validation rules match actual runtime validation.
## Why check?
- **Fluent API** — chain validators, reduce boilerplate
- **Tag verification** — `Check[T]` catches forgotten fields at runtime
- **Zero reflection** — validation is function calls, fully visible and debuggable
- **Type-safe generics** — `Min[T]`, `Between[T]`, `Each[T]` catch type errors at compile time
- **Composable** — `All()` collects errors, `First()` fails fast, nest them freely
- **Field-aware errors** — every error knows which field failed and why
- **Validation tracking** — verify which validators ran on which fields
## Validation as Code
Check enables a pattern: **validation logic as visible, testable code**.
Your validation rules live in methods alongside your types. They're testable, refactorable, and readable. No tag DSL to learn, no reflection overhead, no magic.
```go
// Conditional validation — just Go code
func (o Order) Validate() error {
validations := []*check.Validation{
check.Str(o.ID, "id").Required().UUID().V(),
check.Num(o.Total, "total").Positive().V(),
}
if o.ShipmentType == "express" {
validations = append(validations,
check.Str(o.ExpressCode, "express_code").Required().V(),
)
}
return check.All(validations...).Err()
}
// Cross-field validation — just compare values
func (r DateRange) Validate() error {
return check.All(
check.NotZeroTime(r.Start, "start"),
check.NotZeroTime(r.End, "end"),
check.GreaterThanField(r.End, r.Start, "end", "start"),
).Err()
}
// Slice element validation — auto-generated field names
func (c Cart) Validate() error {
return check.All(
check.StrSlice(c.ItemIDs, "items").NotEmpty().Each(func(b *check.StrBuilder) {
b.Required().UUID()
}).V(),
).Err()
}
```
The compiler checks your validation logic. Your IDE can navigate to it. Your tests can exercise it directly.
## Documentation
- [pkg.go.dev reference](https://pkg.go.dev/github.com/zoobz-io/check)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
MIT License — see [LICENSE](LICENSE) for details.