Installation
To install model, simply run:
go get github.com/ygrebnov/model
This will fetch the latest version of the library and add it to your module dependencies.
Requirements:
- Go 1.22 or higher.
After installation, you can import the package into your project:
import "github.com/ygrebnov/model"
Constructor: New
import (
"context"
"errors"
"github.com/ygrebnov/model"
modelerrors "github.com/ygrebnov/model/errors"
modelvalidation "github.com/ygrebnov/model/validation"
)
ctx := context.Background()
m, err := model.New(&user,
model.WithDefaults[User](),
model.WithValidation[User](ctx), // run validation during New() with cancellation support
)
if err != nil {
var ve *modelvalidation.Error
switch {
case errors.Is(err, modelerrors.ErrNilObject):
// handle nil object
case errors.Is(err, modelerrors.ErrNotStructPtr):
// handle pointer to non-struct
case errors.As(err, &ve):
// handle field validation failures
default:
// defaults parsing or other errors
}
}
To validate later explicitly, call m.Validate(ctx) with a context appropriate for the request.
If you want a reusable engine for many values of the same type, prefer model.NewBinding[T]() and then call ApplyDefaults, Validate, or ValidateWithDefaults.
Why no MustNew?
MustNew (a variant that panics on configuration errors) is intentionally omitted:
- Panics hinder graceful startup error reporting in services / CLIs.
- All failure modes (
nilobject, non-struct pointer, duplicate rule overload, validation failures when requested) are ordinary and recoverable. - Returning
errorkeeps initialization explicit and test-friendly (you can assert exact sentinel errors or unwrap*validation.Error). - If you truly want a panic wrapper, you can write a 2‑line helper in your own code:
func MustNew[T any](o *T, opts ...model.Option[T]) *model.Model[T] {
m, err := model.New(o, opts...); if err != nil { panic(err) }; return m
}
Functional options
All options run in the order provided. If an option returns an error (e.g., attempting to register a duplicate overload for the same type & name), New stops and returns that error.
WithDefaults[T]() — apply defaults during construction
m, err := model.New(&u, model.WithDefaults[User]())
- Runs once per
Model(guarded bysync.Once). - Writes only zero values.
WithValidation[T](ctx context.Context) — run validation during construction
ctx := context.Background()
m, err := model.New(&u,
model.WithValidation[User](ctx),
)
- Gathers all field errors; returns a
*validation.Erroron failure. - Built-ins are always considered first for matching types.
- Cancellation/deadlines follow the provided context.
- To override a built-in rule, register a custom rule before
WithValidation:
minCustom, _ := validation.NewRule[string]("min", func(s string, params ...string) error {
if len(params) == 0 { return fmt.Errorf("min requires 1 param") }
n, _ := strconv.Atoi(params[0])
if len(strings.TrimSpace(s)) < n { return fmt.Errorf("must be at least %d chars after trimming", n) }
return nil
})
m, err := model.New(&u,
model.WithRules[User](minCustom), // override builtin string min
model.WithValidation[User](ctx),
)
WithRules[T](rules ...Rule) — register one or many rules
Create rules with NewRule and pass them. Duplicate exact overloads (same rule name & identical field type) are rejected.
maxLen, _ := validation.NewRule[string]("maxLen", func(s string, params ...string) error {
if len(params) < 1 { return fmt.Errorf("maxLen requires 1 param") }
n, _ := strconv.Atoi(params[0])
if len(s) > n { return fmt.Errorf("must be <= %d chars", n) }
return nil
})
positive64, _ := validation.NewRule[int64]("positive", func(v int64, _ ...string) error {
if v <= 0 { return fmt.Errorf("must be > 0") }
return nil
})
m, _ := model.New(&u,
model.WithRules[User](maxLen, positive64),
)
Model methods
SetDefaults() error
Apply default:"…" / defaultElem:"…" recursively. Safe to call multiple times (subsequent calls no-op).
Validate(ctx context.Context) error
Walk fields and apply rules from validate:"…" / validateElem:"…" tags. Returns *validation.Error on failure.
- Returns
ctx.Err()immediately if the context is canceled or its deadline is exceeded.
Binding[T]
For repeated validation/defaulting of the same type, build a reusable binding:
b, err := model.NewBinding[User]()
if err != nil {
return err
}
if err := b.ValidateWithDefaults(context.Background(), &user); err != nil {
return err
}
Struct tags (how it works)
Defaults: default:"…" and defaultElem:"…"
- Literals: string, bool, ints/uints, floats,
time.Duration. dive: recurse into struct or*struct(allocating a new struct for nil pointers).alloc: allocate empty slice/map if nil.defaultElem:"dive": recurse into struct elements (slice/array) or map values.
Pointer-to-scalar fields (e.g., *int, *bool) are auto-allocated for literal defaults when nil. Pointer-to-complex types (struct/map/slice) are not auto-allocated for literals.
Validation: validate:"…" and validateElem:"…"
- Comma-separated top-level rules.
- Parameters in parentheses:
rule(p1,p2). - Empty tokens skipped (
,nonempty,→nonempty). validateElem:"dive"recurses into struct elements; non-struct or nil pointer elements produce a misuse error under rule namedive.
Built-in rules
Built-in rules are always implicitly available (you do not need to register or import anything for them):
- String:
min(...),max(...),oneof(...),email,uuid - Int / Int64 / Float64:
min(...),max(...),nonzero,oneof(...)
Overriding: if you register a custom rule with the same name and exact type before validation runs, your rule is chosen (duplicate exact registrations for the same name & type are rejected). Interface-based rules still participate via assignable matching when no exact rule exists.
Under the hood, many builtin and internal errors are produced with structured metadata using the companion errorc and keys projects.
Custom rules (with parameters)
minLen, _ := validation.NewRule[string]("minLen", func(s string, params ...string) error {
if len(params) < 1 { return fmt.Errorf("minLen requires 1 param") }
n, err := strconv.Atoi(params[0])
if err != nil { return fmt.Errorf("minLen: bad param: %w", err) }
if len(s) < n { return fmt.Errorf("must be at least %d chars", n) }
return nil
})
type Payload struct { Body string ` + "`" + `validate:"minLen(3)"` + "`" + ` }
p := Payload{Body: "xy"}
m, _ := model.New(&p, model.WithRules[Payload](minLen))
if err := m.Validate(context.Background()); err != nil {
fmt.Println(err) // Body: must be at least 3 chars (rule minLen)
}
Interface rules:
type stringer interface{ String() string }
stringerRule, _ := validation.NewRule[stringer]("stringerBad", func(s stringer, _ ...string) error {
if s.String() == "" { return fmt.Errorf("empty") }
return fmt.Errorf("bad stringer: %s", s.String()) // demo
})
m, _ := model.New(&obj, model.WithRules[YourType](stringerRule))
Companion packages
In addition to the main package, you may also import:
github.com/ygrebnov/model/errorsfor sentinel errors such asErrNilObjectgithub.com/ygrebnov/model/validationforvalidation.Errorandvalidation.NewRule
The project itself is built on top of errorc and keys, which is useful if you want to inspect or transform low-level structured validation errors.