SkillAgentSearch skills...

Shak

A type-safe Go validation library built on generics and higher-order functions. Uses plain Go constructs — no reflection, no struct tags — so validation rules are just functions that compose naturally with the rest of your code.

Install / Use

/learn @yashx/Shak
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

shak

Go Reference Go Report Card Go Version License GitHub Release

shak (pronounced like "shuck") is a Hindi word (शक) that means doubt.

Description

shak is a Go validation library built on generics and higher-order functions — no reflection, no struct tags, no magic. Rules are plain functions, and the type system enforces correctness at compile time.

It has the following features:

  • Type-safe by design — powered by Go generics, not reflection.
  • Use normal programming constructs rather than struct tags to specify validation rules.
  • Validate data of different types — primitives, strings, slices, maps, structs, pointers, and more.
  • Validate custom types by implementing the Validatable interface.
  • Compose rules with And, Or, If, and Else for expressive conditional logic.
  • Collect all errors at once or stop at the first failure.
  • Structured errors with field paths for precise error reporting.
  • Rich set of built-in rules, and easy to add custom ones.

Installation

go get github.com/yashx/shak

Requirements

Go 1.21 or above.

A Note on LLM Usage

The design of this library — its architecture, core abstractions, and the initial set of validation rules — was written by me. LLMs were used to speed up the parts of development I don't find interesting and didn't want to spend my weekends on:

  • Tests — unit and integration test coverage
  • Documentation — doc comments and this README
  • Code review — catching bugs and inconsistencies
  • Built-in rules — filling out common use cases beyond the initial set

Getting Started

shak is built around two concepts: rules and validations. A rule is a function that checks a single value. A validation groups rules and fields together and can be executed to collect errors.

  • Use shak.Validate() / shak.ValidateAll() to validate a single value against rules directly.
  • Use shak.RunValidation() / shak.RunFullValidation() to execute a structured validation built with validation.Value(), validation.Nested(), and validation.NewValidations().
  • Implement Validatable on your structs to make them self-describing.

Validating a Simple Value

Use shak.Validate() to validate a single value against one or more rules:

package main

import (
    "fmt"

    "github.com/yashx/shak"
    "github.com/yashx/shak/rule"
)

func main() {
    age := 150
    err := shak.Validate(age,
        rule.Min(0),   // must be >= 0
        rule.Max(120), // must be <= 120
    )
    fmt.Println(err)
    // Output:
    // value 150 is greater than maximum 120
}

Rules run in order. The first failure is returned and the rest are skipped. Use shak.ValidateAll() to collect every error instead:

score := 150
errs := shak.ValidateAll(score,
    rule.Min(200), // must be >= 200
    rule.Max(100), // must be <= 100
)
for _, err := range errs {
    fmt.Println(err)
}
// Output:
// value 150 is less than minimum 200
// value 150 is greater than maximum 100

Validating a Struct

To validate a struct, implement the Validatable interface by defining a Validation() method that describes the rules for each field. Then pass it to shak.RunValidation() or shak.RunFullValidation():

type Address struct {
    Street string
    City   string
}

func (a Address) Validation() validation.Validation {
    return validation.NewValidations(
        validation.Value("street", a.Street, rule.NotBlank[string](), rule.MinLengthString[string](5)),
        validation.Value("city",   a.City,   rule.NotBlank[string]()),
    )
}

a := Address{Street: "123", City: ""}

err := shak.RunValidation(a)
fmt.Println(err)
// Output:
// street: length is 3 bytes, expected at least 5

errs := shak.RunFullValidation(a)
for _, err := range errs {
    fmt.Println(err)
}
// Output:
// street: length is 3 bytes, expected at least 5
// city: value must not be blank

Fields are validated in the order they are declared. RunValidation stops at the first failure; RunFullValidation collects errors across all fields.

Nested Structs

When a struct field is itself a Validatable, use validation.Nested() to include its own validation rules. Errors from the inner struct are automatically prefixed with the field name:

type Address struct {
    Street string
    City   string
}

func (a Address) Validation() validation.Validation {
    return validation.NewValidations(
        validation.Value("street", a.Street, rule.NotBlank[string]()),
        validation.Value("city",   a.City,   rule.NotBlank[string]()),
    )
}

type Order struct {
    ID      int
    Address Address
}

func (o Order) Validation() validation.Validation {
    return validation.NewValidations(
        validation.Value("ID",      o.ID,      rule.Positive[int]()),
        validation.Nested("address", o.Address),
    )
}

o := Order{ID: 1, Address: Address{Street: "", City: "Austin"}}

err := shak.RunValidation(o)
fmt.Println(err)
// address.street: value must not be blank

You can also pass outer rules to Nested. They run before the field's own Validation() and receive the whole struct as the value:

validCities := []string{"Austin", "Denver", "Portland"}

func mustBeServicedCity(a Address) *types.ValidationError {
    for _, c := range validCities {
        if a.City == c {
            return nil
        }
    }
    return types.NewValidationError("delivery not available in %q", a.City)
}

func (o Order) Validation() validation.Validation {
    return validation.NewValidations(
        validation.Value("ID",       o.ID,      rule.Positive[int]()),
        validation.Nested("address", o.Address, mustBeServicedCity),
    )
}

o := Order{ID: 1, Address: Address{Street: "1 Oak Ave", City: "Miami"}}

err := shak.RunValidation(o)
fmt.Println(err)
// address: delivery not available in "Miami"

Value vs Nested for a Validatable Field

When a field's type implements Validatable, you have two choices:

  • validation.Nested — runs any rules you pass and calls the field's own Validation(). Use this when you want the field to be fully validated as it defines itself.
  • validation.Value — runs only the rules you explicitly pass. The field's own Validation() is never called. Use this when you want to apply a different or reduced set of rules in a specific context.
// Nested: Address.Validation() runs — street and city are both checked
validation.Nested("address", o.Address)

// Value: only your rule runs — Address.Validation() is ignored
validation.Value("address", o.Address, func(a Address) *types.ValidationError {
    if a.City == "" {
        return types.NewValidationError("city is required for delivery")
    }
    return nil
})

Checking Validity Without Errors

When you only need a boolean result and don't care about the error details, use shak.Satisfies() or shak.SatisfiesValidation():

// for a single value
if shak.Satisfies(age, rule.Min(0), rule.Max(120)) {
    fmt.Println("age is valid")
}

// for a struct validation
if shak.SatisfiesValidation(address) {
    fmt.Println("address is valid")
}

Validating Maps

shak provides rules for validating map keys, values, and specific entries. Map rules take a pointer to the map as the first argument to help the compiler infer types:

scores := map[string]int{"alice": 95, "bob": -1}

// validate every value
err := shak.Validate(scores, rule.ForEachValue(&scores, rule.Min(0)))
// [bob]: value -1 is less than minimum 0

// validate every key
err = shak.Validate(scores, rule.ForEachKey(&scores, rule.NotBlank[string]()))

// validate a specific key
err = shak.Validate(scores, rule.ForKey(&scores, "alice", rule.Max(100)))

Other available map rules:

  • rule.LengthMap(&m, n) — map must have exactly n entries
  • rule.MinLengthMap(&m, n) — map must have at least n entries
  • rule.MaxLengthMap(&m, n) — map must have at most n entries
  • rule.EqualMap(m2) — map must equal m2
  • rule.EqualMapFunc(&m, m2, eq) — map must equal m2 using a custom equality function

Validating Slices

Like map rules, slice rules take a pointer to the slice as the first argument for type inference:

prices := []float64{9.99, -1.00, 49.99}

// validate every element
err := shak.Validate(prices, rule.ForEach(&prices, rule.Positive[float64]()))
// [1]: value -1 is not positive

// check length
err = shak.Validate(prices, rule.MinLengthSlice(&prices, 1))

// check for duplicates
tags := []string{"go", "go", "validation"}
err = shak.Validate(tags, rule.Unique(&tags))
// duplicate value go found

Other available slice rules:

  • rule.LengthSlice(&s, n) — slice must have exactly n elements
  • rule.MaxLengthSlice(&s, n) — slice must have at most n elements
  • rule.ContainsElem(&s, v) — slice must contain v
  • rule.NotContainsElem(&s, v) — slice must not contain v
  • rule.EqualSlice(s2) — slice must equal s2
  • rule.EqualSliceFunc(&s, s2, eq) — slice must equal s2 using a custom equality function
  • rule.IsSorted(&s) — slice must be sorted in ascending order
  • rule.IsSortedFunc(&s, cmp) — slice must be sorted by a custom comparison function
  • rule.MatchBytes(re) — byte slice must match a regular expression
  • rule.NotMatchBytes(re) — byte slice must not match a regular expression

Validating a Slice or Map of Validatable Items

Use rule.IsValid[T]() to treat each element's own Validation() as a rule. This composes naturally with ForEach and ForEachValue:

type Pr
View on GitHub
GitHub Stars62
CategoryDevelopment
Updated14d ago
Forks1

Languages

Go

Security Score

100/100

Audited on Mar 17, 2026

No findings