GoZod is a TypeScript Zod v4-inspired validation library for Go, providing strongly-typed data validation with intelligent type inference and maximum performance.
- TypeScript Zod v4 Compatible API - Familiar syntax with Go-native optimizations
- Complete Strict Type Semantics - All methods require exact input types, zero automatic conversions
- π·οΈ Declarative Struct Tags - Define validation rules directly on struct fields with
gozod:"required,min=2,email" - Parse vs StrictParse - Runtime flexibility or compile-time type safety for optimal performance
- Native Go Struct Support - First-class struct validation with field-level validation and JSON tag mapping
- Automatic Circular Reference Handling - Lazy evaluation prevents stack overflow in recursive structures
- Maximum Performance - Zero-overhead validation with optional code generation (5-10x faster)
- Curated Dependencies - Small, intentional dependency surface with Go-first implementation
- Rich Validation Methods - Comprehensive built-in validators for all Go types
- π― Type Safety First - Compile-time guarantees with runtime flexibility
- β‘ Maximum Performance - Zero-overhead abstractions with optional code generation
- π·οΈ Developer Experience - Familiar API with Go idioms and declarative struct tags
- π Production Ready - Battle-tested validation with comprehensive error handling
- π Go-First Implementation - Minimal, intentional dependencies and predictable behavior
GoZod brings TypeScript Zod's excellent developer experience to Go while embracing Go's type system and performance characteristics. Perfect for API validation, configuration parsing, data transformation, and any scenario where type-safe validation is critical.
go get github.com/kaptinlin/gozodpackage main
import (
"fmt"
"github.com/kaptinlin/gozod"
)
func main() {
// String validation with method chaining
nameSchema := gozod.String().Min(2).Max(50)
// Parse - Runtime type checking (flexible)
result, err := nameSchema.Parse("Alice")
if err == nil {
fmt.Println("Valid name:", result) // "Alice"
}
// StrictParse - Compile-time type safety (optimal performance)
name := "Alice"
result, err = nameSchema.StrictParse(name) // Input type guaranteed
if err == nil {
fmt.Println("Validated name:", result)
}
// Email validation
emailSchema := gozod.String().Email()
email := "user@example.com"
result, err = emailSchema.StrictParse(email)
if err == nil {
fmt.Printf("Valid email: %s\n", result)
}
}package main
import (
"fmt"
"github.com/kaptinlin/gozod"
)
type User struct {
Name string `gozod:"required,min=2,max=50"`
Email string `gozod:"required,email"`
Age int `gozod:"required,min=18,max=120"`
Bio string `gozod:"max=500"` // Non-required => generated Optional()
}
func main() {
// Generate schema from struct tags
schema := gozod.FromStruct[User]()
user := User{
Name: "Alice Johnson",
Email: "alice@example.com",
Age: 28,
Bio: "Software engineer",
}
// Validate with generated schema
validatedUser, err := schema.Parse(user)
if err != nil {
fmt.Printf("Validation error: %v\n", err)
return
}
fmt.Printf("Valid user: %+v\n", validatedUser)
}For tag-derived schemas and gozodgen, structural tag rules such as required and coerce are handled separately from validation rules. The generated validation chain is built from the remaining rules, and .Optional() is appended for non-required or pointer fields.
For scenarios where the struct type is only known at runtime (e.g., CLI frameworks, web frameworks):
package main
import (
"fmt"
"github.com/kaptinlin/gozod/types"
)
type Config struct {
Host string `validate:"required,min=1"`
Port int `validate:"min=1000,max=9999"`
}
func main() {
config := &Config{Host: "localhost", Port: 8080}
// Runtime validation without generic type parameters
result, err := types.ValidateStruct(config, types.WithTagName("validate"))
if err != nil {
fmt.Printf("Validation error: %v\n", err)
return
}
// Type assertion needed for runtime validation
validated := result.(Config)
fmt.Printf("Valid config: %+v\n", validated)
}package main
import (
"fmt"
"github.com/kaptinlin/gozod"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
func main() {
// Basic struct validation
basicSchema := gozod.Struct[User]()
user := User{Name: "Alice", Age: 25, Email: "alice@example.com"}
result, err := basicSchema.Parse(user)
if err == nil {
fmt.Printf("Basic validation: %+v\n", result)
}
// Struct with field validation
userSchema := gozod.Struct[User](gozod.StructSchema{
"name": gozod.String().Min(2).Max(50),
"age": gozod.Int().Min(0).Max(120),
"email": gozod.String().Email(),
})
validUser := User{Name: "Bob", Age: 30, Email: "bob@example.com"}
result, err = userSchema.Parse(validUser)
if err == nil {
fmt.Printf("Field validation: %+v\n", result)
}
}GoZod uses strict type semantics - no automatic conversions between types:
// Value schemas require exact value types
stringSchema := gozod.String()
result, _ := stringSchema.Parse("hello") // β
string β string
// result, _ := stringSchema.Parse(&str) // β Error: requires string, got *string
// Pointer schemas require exact pointer types
stringPtrSchema := gozod.StringPtr()
result, _ = stringPtrSchema.Parse(&str) // β
*string β *string
// result, _ = stringPtrSchema.Parse("hello") // β Error: requires *string, got string
// For flexible input handling, use Optional/Nilable modifiers
optionalSchema := gozod.String().Optional() // Flexible input, *string output
result, _ = optionalSchema.Parse("hello") // β
string β *string (new pointer)
result, _ = optionalSchema.Parse(&str) // β
*string β *string (preserves identity)Choose the right parsing method for your use case:
schema := gozod.String().Min(3)
// Parse - Runtime type checking (flexible input)
result, err := schema.Parse("hello") // β
Works with any input type
result, err = schema.Parse(42) // β Runtime error: invalid type
// StrictParse - Compile-time type safety (optimal performance)
str := "hello"
result, err = schema.StrictParse(str) // β
Compile-time guarantee, optimal performance
// result, err := schema.StrictParse(42) // β Compile-time errorUse .Refine() for custom validation logic:
package main
import (
"strings"
"github.com/kaptinlin/gozod"
)
func main() {
// Custom validation with Refine
usernameSchema := gozod.String().
Min(3).
Max(20).
Refine(func(username string) bool {
// Check against blacklist
blacklist := map[string]bool{"admin": true, "root": true}
return !blacklist[strings.ToLower(username)]
}, "Username is not allowed")
// Valid username
result, err := usernameSchema.Parse("johndoe") // β
Valid
// Invalid username
_, err = usernameSchema.Parse("admin") // β Validation fails
if err != nil {
fmt.Printf("Validation failed: %v\n", err)
}
// Struct validation with custom logic
type User struct {
Name string `gozod:"required,min=2"`
Email string `gozod:"required,email"`
Age int `gozod:"min=18"`
}
schema := gozod.FromStruct[User]().Refine(func(user User) bool {
// Cross-field validation
return user.Age >= 21 || !strings.Contains(user.Email, "company.com")
}, "Users under 21 cannot have company emails")
}
### Automatic Circular Reference Handling
GoZod automatically detects and handles circular references:
```go
type User struct {
Name string `gozod:"required,min=2"`
Email string `gozod:"required,email"`
Friends []*User `gozod:"max=10"` // Circular reference
}
// No stack overflow - automatically uses lazy evaluation
schema := gozod.FromStruct[User]()
alice := &User{Name: "Alice", Email: "alice@example.com"}
bob := &User{Name: "Bob", Email: "bob@example.com", Friends: []*User{alice}}
alice.Friends = []*User{bob} // Circular reference
result, err := schema.Parse(*alice) // β
Handles circular reference safely// Union types - accepts one of multiple schemas (any match succeeds)
unionSchema := gozod.Union(
gozod.String(),
gozod.Int(),
)
result, _ := unionSchema.Parse("hello") // β
Matches string
result, _ = unionSchema.Parse(42) // β
Matches int
result, _ = unionSchema.Parse(true) // β No union member matched
// Xor types - exactly one schema must match (exclusive union)
xorSchema := gozod.Xor([]any{
gozod.Email(), // Email format validator
gozod.URL(), // URL format validator
})
result, _ = xorSchema.Parse("user@example.com") // β
Matches email only
result, _ = xorSchema.Parse("https://site.com") // β
Matches URL only
result, _ = xorSchema.Parse("invalid") // β Matches neither
// Intersection types - must satisfy all schemas
intersectionSchema := gozod.Intersection(
gozod.String().Min(3), // At least 3 chars
gozod.String().Max(10), // At most 10 chars
gozod.String().RegexString(`^[a-z]+$`), // Only lowercase
)
result, _ = intersectionSchema.Parse("hello") // β
Satisfies all constraints
result, _ = intersectionSchema.Parse("HELLO") // β Not lowercase
// And/Or methods - fluent composition on any schema type
schema := gozod.String().Min(3).And(gozod.String().Max(10)) // Intersection via method
schema = gozod.Int().Or(gozod.String()) // Union via methodFor maximum performance, use code generation:
//go:generate gozodgen
type User struct {
Name string `gozod:"required,min=2"`
Email string `gozod:"required,email"`
Age int `gozod:"required,min=18"`
}
// Generated Schema() method provides zero-reflection validation
func main() {
schema := gozod.FromStruct[User]() // Uses generated code automatically
user := User{Name: "Alice", Email: "alice@example.com", Age: 25}
result, err := schema.Parse(user) // 5-10x faster than reflection
}// Strings with format validation
gozod.String().Min(3).Max(100).Email()
gozod.String().RegexString(`^\d+$`)
gozod.String().Lowercase() // Validates no uppercase letters
gozod.String().Uppercase() // Validates no lowercase letters
gozod.String().Normalize() // Unicode NFC normalization
gozod.Uuid() // UUID format validator
gozod.Guid() // GUID format validator (8-4-4-4-12 hex)
gozod.URL() // URL format validator
gozod.HTTPURL() // HTTP/HTTPS URL only
gozod.Email() // Email format validator
// Network formats
gozod.IPv4() // IPv4: "192.168.1.1"
gozod.IPv6() // IPv6: "2001:db8::8a2e:370:7334"
gozod.Hostname() // DNS hostname: "example.com"
gozod.MAC() // MAC address: "00:1A:2B:3C:4D:5E"
gozod.E164() // E.164 phone: "+14155552671"
gozod.CIDRv4() // IPv4 CIDR: "192.168.1.0/24"
// Text encodings and hashes
gozod.Hex() // Hexadecimal string
gozod.Base64() // Base64 encoding
gozod.JWT() // JWT token format
// Numbers with range validation
gozod.Int().Min(0).Max(120).Positive()
gozod.Float64().Min(0.0).Finite()
// Booleans
gozod.Bool()
// Time validation
gozod.Time().After(startDate).Before(endDate)// Arrays and Slices
gozod.Array(gozod.String()).Min(1).Max(10)
gozod.Slice(gozod.Int()).NonEmpty()
// Tuples - Fixed-length arrays with typed elements
tuple := gozod.Tuple(gozod.String(), gozod.Int(), gozod.Bool())
result, _ := tuple.Parse([]any{"hello", 42, true})
// Tuple with rest element for variadic trailing elements
tupleWithRest := gozod.TupleWithRest(
[]core.ZodSchema{gozod.String(), gozod.Int()},
gozod.Bool(), // additional elements must be booleans
)
// Maps
gozod.Map(gozod.String()).NonEmpty() // map[string]string, at least one entry
gozod.Map(gozod.Struct[User]()) // map[string]User
// Records with key validation
gozod.Record(gozod.String().Regex(`^[a-z]+$`), gozod.Int()) // lowercase keys only
// LooseRecord - passes through non-matching keys unchanged
gozod.LooseRecord(gozod.String().Regex(`^S_`), gozod.String())
// PartialRecord - allows missing keys for exhaustive key schemas
keys := gozod.Enum("id", "name", "email")
gozod.Record(keys, gozod.String()).Partial() // Missing keys allowed
// Sets - Go idiomatic set pattern with element validation
gozod.Set[string](gozod.String()).Min(1).Max(10) // map[string]struct{}
// Objects (map[string]any)
gozod.Object(gozod.ObjectSchema{
"name": gozod.String().Min(2),
"age": gozod.Int().Min(0),
})// Transform types
stringToInt := gozod.String().Regex(`^\d+$`).Transform(
func(s string, ctx *core.RefinementContext) (any, error) {
return strconv.Atoi(s)
},
)
// Lazy types for recursive structures
var nodeSchema gozod.ZodType[Node]
nodeSchema = gozod.Lazy(func() gozod.ZodType[Node] {
return gozod.Struct[Node](gozod.StructSchema{
"value": gozod.String(),
"children": gozod.Array(nodeSchema), // Self-reference
})
})
// Schema metadata
schema := gozod.String().Email().Describe("User's primary email")
schema = gozod.Int().Meta(gozod.GlobalMeta{
Title: "Age",
Description: "User's age in years",
})
// Apply - Compose reusable schema modifiers
func addCommonChecks[T types.StringConstraint](s *gozod.ZodString[T]) *gozod.ZodString[T] {
return s.Min(1).Max(100).Trim()
}
schema := gozod.Apply(gozod.String(), addCommonChecks)Comprehensive error information with structured details:
schema := gozod.String().Min(5).Email()
_, err := schema.Parse("hi")
if zodErr, ok := err.(*issues.ZodError); ok {
// Access structured error information
for _, issue := range zodErr.Issues {
fmt.Printf("Path: %v, Code: %s, Message: %s\n",
issue.Path, issue.Code, issue.Message)
}
// Pretty print errors
fmt.Println(zodErr.PrettifyError())
// Get flattened field errors for forms
fieldErrors := zodErr.FlattenError()
for field, errors := range fieldErrors.FieldErrors {
fmt.Printf("%s: %v\n", field, errors)
}
}required- Field must be presentmin=N/max=N- Length constraintsemail/url/uuid- Format validationregex=pattern- Custom regex patterns
min=N/max=N- Value constraintspositive/negative- Sign validationnonnegative/nonpositive- Zero-inclusive constraints
min=N/max=N- Element count constraintsnonempty- At least one elementlength=N- Exact element count
Use .Refine() for custom validation logic on any schema:
schema := gozod.FromStruct[Product]().Refine(func(p Product) bool {
return strings.HasPrefix(p.SKU, "PROD-")
}, "SKU must start with PROD-")type CreateUserRequest struct {
Name string `json:"name" gozod:"required,min=2,max=50"`
Email string `json:"email" gozod:"required,email"`
Age int `json:"age" gozod:"required,min=18,max=120"`
Tags []string `json:"tags" gozod:"max=10"`
Website string `json:"website" gozod:"url"`
IsActive bool `json:"is_active"`
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
schema := gozod.FromStruct[CreateUserRequest]()
validatedReq, err := schema.Parse(req)
if err != nil {
writeValidationError(w, err)
return
}
user := createUser(validatedReq)
json.NewEncoder(w).Encode(user)
}type Config struct {
Environment string `yaml:"environment" validate:"required,regex=^(dev|staging|prod)$"`
Port int `yaml:"port" validate:"required,min=1000,max=9999"`
Database struct {
Host string `yaml:"host" validate:"required"`
Port int `yaml:"port" validate:"required,min=1,max=65535"`
Name string `yaml:"name" validate:"required,min=1"`
Username string `yaml:"username" validate:"required"`
Password string `yaml:"password" validate:"required,min=8"`
} `yaml:"database" validate:"required"`
Debug bool `yaml:"debug"`
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
schema := gozod.FromStruct[Config](gozod.WithTagName("validate"))
return schema.Parse(config)
}- API Reference - Complete API documentation with all methods and examples
- Struct Tags Guide - Comprehensive tag syntax and custom validator integration
- Feature Mapping - Complete TypeScript Zod v4 to GoZod mapping reference
- Basics Guide - Getting started with core concepts and patterns
- Error Customization - Custom error messages and internationalization
- Error Formatting - Structured error handling and display
- JSON Schema - Generate JSON Schema from GoZod schemas
GoZod schemas generate JSON Schema Draft 2020-12, which is fully compatible with OpenAPI 3.1. Features like nullable types (["string", "null"]), numeric exclusive bounds, conditional schemas (if/then/else), and tuple validation work out of the box. See JSON Schema docs for details.
Note: OpenAPI 3.0 is not supported (use OpenAPI 3.1 instead).
- Metadata - Schema metadata and introspection capabilities
- Examples - Working examples for common use cases
GoZod is designed for maximum performance:
- Curated Dependencies - Minimal dependency surface and predictable runtime behavior
- Strict Type Semantics - No runtime type conversions
- StrictParse Method - Compile-time type safety eliminates runtime checks
- Code Generation - Optional zero-reflection validation (5-10x faster)
- Efficient Validation Pipeline - Optimized execution paths
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.