Skip to content
/ check Public

A validation library for Go that doesn't suck. Inspired by Zod, built for Go.

License

Notifications You must be signed in to change notification settings

Alinxus/check

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

check

A validation library for Go that doesn't suck. Inspired by Zod, built for Go.

Why This Exists

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.

Install

go get github.com/Alinxus/check

Then import:

import "github.com/Alinxus/check/z"

Quick Start

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-converted

Core Concepts

Schemas

A schema is a validator. Every schema implements the Schema interface with two methods:

  • Parse(value any) (any, error) - Validate and return the parsed value
  • Validate(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.

Chainable API

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 constraints

This immutability pattern prevents subtle bugs where you accidentally share configuration between schemas.

Type Coercion

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 true

Transforms

After 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.

Schema Types

String

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 { ... })

Int

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 { ... })

Float

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 { ... })

Bool

Boolean values:

z.Bool()                   // required boolean
z.Bool().Optional()
z.Bool().Default(false)

Time

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 { ... })

Any

Accepts any non-nil value (rarely needed):

z.Any()                    // accepts anything non-nil
z.Any().Optional()         // accepts anything including nil

Array

Validate 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 items

Errors for array elements include the index:

Validation failed:
  - colors[1]: must be one of [red, green, blue] (got "yellow")

Object

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()

Map

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)

Union

A value matching any one of several schemas:

z.Union(z.String(), z.Int())  // string or int

Parsing Methods

Parse

Validate and return the parsed value. Fails fast on the first error:

result, err := schema.Parse(data)
if err != nil {
    fmt.Println(err)
}

Validate

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)
}

ParseJSON

Validate raw JSON bytes:

jsonBytes := []byte(`{"name": "Alice", "email": "alice@example.com"}`)
result, err := z.ParseJSON(schema, jsonBytes)

ParseStruct

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 populated

ParseTyped

Type-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 any

Real-World Example

A 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)
}

Error Messages

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
}

Design Decisions

No Dependencies

Zero external dependencies. The library only uses Go's standard library. This keeps it simple, fast, and easy to integrate.

Immutability Through Cloning

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.

All Errors at Once

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.

Type Conversion, Not Coercion

The coerce schemas explicitly convert types (string to int, etc). They don't try to be clever. This makes the behavior predictable.

Go Idioms

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

Limitations

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)

Contributing

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.

License

MIT. Use it however you want, commercially or otherwise. See LICENSE file.

About

A validation library for Go that doesn't suck. Inspired by Zod, built for Go.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages