A validation library for Go that doesn't suck. Inspired by Zod, built for Go.
Go's standard library is great, but validation gets messy fast. You either write verbose code with lots of if statements, or you grab a library that requires learning struct tags and reflection magic.
check takes a different approach. Define schemas in code with a fluent API. Chain validators together. Get all validation errors at once, not just the first failure. The API looks similar to JavaScript's Zod if you've used that, but it's designed from the ground up for Go.
go get github.com/Alinxus/checkThen import:
import "github.com/Alinxus/check/z"schema := z.Object(map[string]z.Schema{
"name": z.String().Min(3).Max(50),
"email": z.String().Email(),
"age": z.Int().Min(0).Max(150).Optional(),
})
data := map[string]any{
"name": "Alice",
"email": "alice@example.com",
}
result, err := schema.Parse(data)
if err != nil {
fmt.Println(err)
return
}
// result is validated and type-convertedA schema is a validator. Every schema implements the Schema interface with two methods:
Parse(value any) (any, error)- Validate and return the parsed valueValidate(value any) []ValidationError- Return all validation errors without failing fast
The key difference from other libraries: Validate returns a slice of all errors, not just the first one. This is more useful in real applications where you want to show users all problems at once.
All schema methods are chainable. They return modified copies of the schema (not mutating the original), so you can safely reuse base schemas:
baseString := z.String().Min(3)
username := baseString.Max(20) // independent copy
slug := baseString.Max(50).Trim() // different constraintsThis immutability pattern prevents subtle bugs where you accidentally share configuration between schemas.
When validation is strict about types (which Go is), type coercion schemas bridge the gap. Perfect for form data, query parameters, or environment variables where everything arrives as strings:
z.CoerceInt().Min(0).Max(100) // "42" becomes int 42
z.CoerceFloat().Min(0) // "3.14" becomes float64 3.14
z.CoerceBool() // "true", "yes", "1", "on" all become trueAfter validation passes, transform the value to something else:
price := z.Transform(z.Float().Min(0), func(v any) (any, error) {
return fmt.Sprintf("$%.2f", v.(float64)), nil
})
result, _ := price.Parse(19.99)
// result == "$19.99"Transforms are useful for normalizing data, computing derived values, or converting types.
Basic string validation with common checks:
z.String() // required string
z.String().Optional() // can be nil
z.String().Default("fallback") // nil becomes "fallback"
z.String().Min(3) // at least 3 chars
z.String().Max(50) // at most 50 chars
z.String().Email() // valid email
z.String().URL() // valid URL
z.String().UUID() // valid UUID v4
z.String().Pattern(regexp, "msg") // regex match
z.String().Contains("foo") // must contain substring
z.String().HasPrefix("http") // must start with
z.String().HasSuffix(".com") // must end with
z.String().OneOf("a", "b", "c") // enum
z.String().Trim() // trim whitespace before validation
z.String().ToLower() // lowercase before validation
z.String().ToUpper() // uppercase before validation
z.String().Custom(func(s string) error { ... })Integer validation:
z.Int() // required integer
z.Int().Min(0) // >= 0
z.Int().Max(100) // <= 100
z.Int().Positive() // > 0
z.Int().Negative() // < 0
z.Int().NonZero() // != 0
z.Int().OneOf(1, 2, 3) // enum
z.Int().Optional()
z.Int().Default(42)
z.Int().Custom(func(n int) error { ... })Floating point numbers:
z.Float() // required float64
z.Float().Min(0.0)
z.Float().Max(1.0)
z.Float().Positive()
z.Float().Negative()
z.Float().Optional()
z.Float().Default(3.14)
z.Float().Custom(func(f float64) error { ... })Boolean values:
z.Bool() // required boolean
z.Bool().Optional()
z.Bool().Default(false)Date and time handling with flexible parsing:
z.Time() // required, parses RFC3339 strings
z.Time().Optional()
z.Time().Layout("2006-01-02") // custom time format
z.Time().Layout("2006-01-02", time.RFC3339) // try multiple formats
z.Time().Before(deadline) // must be before
z.Time().After(startDate) // must be after
z.Time().Custom(func(t time.Time) error { ... })Accepts any non-nil value (rarely needed):
z.Any() // accepts anything non-nil
z.Any().Optional() // accepts anything including nilValidate slice elements:
z.Array(z.String()) // array of strings
z.Array(z.Int().Positive()) // array of positive ints
z.Array(z.String()).MinLength(1) // at least 1 item
z.Array(z.String()).MaxLength(10)// at most 10 itemsErrors for array elements include the index:
Validation failed:
- colors[1]: must be one of [red, green, blue] (got "yellow")
Validate structured data with named fields:
z.Object(map[string]z.Schema{
"name": z.String().Min(1),
"email": z.String().Email(),
"age": z.Int().Optional(),
})Field errors include the field path:
Validation failed:
- name: must be at least 1 characters
- email: invalid email format
With nested objects, paths get deeper:
z.Object(map[string]z.Schema{
"user": z.Object(map[string]z.Schema{
"address": z.Object(map[string]z.Schema{
"city": z.String().Min(1),
}),
}),
})Error:
Validation failed:
- user.address.city: must be at least 1 characters
Strict mode rejects unknown fields (useful for APIs):
z.Object(map[string]z.Schema{
"name": z.String(),
}).Strict()Validate dictionaries with key and value constraints:
z.Map(z.String().Min(1), z.Int().Positive())
z.Map(z.String(), z.Any()).MinLength(1).MaxLength(100)A value matching any one of several schemas:
z.Union(z.String(), z.Int()) // string or intValidate and return the parsed value. Fails fast on the first error:
result, err := schema.Parse(data)
if err != nil {
fmt.Println(err)
}Collect all errors without failing fast. Useful for showing users all problems at once:
errs := schema.Validate(data)
for _, e := range errs {
fmt.Printf("%s: %s\n", e.Path, e.Message)
}Validate raw JSON bytes:
jsonBytes := []byte(`{"name": "Alice", "email": "alice@example.com"}`)
result, err := z.ParseJSON(schema, jsonBytes)Validate data and map it into a Go struct using struct tags:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
var user User
err := z.ParseStruct(schema, data, &user)
// user.Name and user.Email are now populatedType-safe parsing with generics. No type assertions needed:
name, err := z.ParseTyped[string](z.String().Min(3), "Alice")
// name is string, not any
age, err := z.ParseTyped[int](z.Int().Positive(), 25)
// age is int, not anyA complete user registration endpoint:
package main
import (
"fmt"
"github.com/Alinxus/check/z"
)
var createUserSchema = z.Object(map[string]z.Schema{
"username": z.String().Min(3).Max(20),
"email": z.String().Email(),
"password": z.String().Min(8),
"age": z.Int().Min(13).Max(120).Optional(),
"role": z.String().OneOf("user", "moderator").Default("user"),
"tags": z.Array(z.String()).MinLength(1).MaxLength(10),
"address": z.Object(map[string]z.Schema{
"street": z.String().Min(1),
"city": z.String().Min(1),
"zip": z.String().Pattern(zipCode, "invalid zip"),
}).Optional(),
})
func handleCreateUser(data map[string]any) error {
var user User
if err := z.ParseStruct(createUserSchema, data, &user); err != nil {
// err contains all validation failures
fmt.Println(err)
return err
}
// user is now fully validated and type-safe
return saveUser(user)
}Validation errors are designed to be helpful and readable:
Validation failed:
- username: must be at least 3 characters (got "ab")
- email: invalid email format
- age: must be at least 13 (got 10)
- tags: array must have at least 1 item(s) (got 0)
The ValidationError type gives you access to individual failures:
type ValidationError struct {
Path string // dot-separated path, e.g. "address.city"
Message string // human-readable message
Value any // the offending value
}Zero external dependencies. The library only uses Go's standard library. This keeps it simple, fast, and easy to integrate.
Every chainable method returns a new schema instance rather than mutating the receiver. This prevents subtle bugs where schemas accidentally share configuration. It's a Go best practice borrowed from functional programming.
Most validators stop at the first error. check collects all validation errors so you can show users everything that's wrong at once. This is better UX for forms, APIs, and batch processing.
The coerce schemas explicitly convert types (string to int, etc). They don't try to be clever. This makes the behavior predictable.
The API uses Go conventions:
- Interfaces for extension
- Slice of errors instead of wrapped errors
- Simple, readable code without magic
- No reflection where it's not needed
This library is for validating Go types. It won't:
- Generate documentation or JSON schemas (future feature maybe)
- Handle async validators or database lookups (by design)
- Parse struct tags (intentional - we do code-based schemas)
- Validate pointers directly (convert to concrete types first)
Bug fixes and suggestions are welcome. The goal is to keep this library simple and focused on validation, not to build a kitchen-sink framework.
MIT. Use it however you want, commercially or otherwise. See LICENSE file.