Skip to main content

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 (nil object, non-struct pointer, duplicate rule overload, validation failures when requested) are ordinary and recoverable.
  • Returning error keeps 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 by sync.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.Error on 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 name dive.

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/errors for sentinel errors such as ErrNilObject
  • github.com/ygrebnov/model/validation for validation.Error and validation.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.