{"id":37871108,"url":"https://github.com/ygrebnov/model","last_synced_at":"2026-01-16T16:40:59.077Z","repository":{"id":313747000,"uuid":"1050278556","full_name":"ygrebnov/model","owner":"ygrebnov","description":"Go library for applying struct defaults and validating fields with simple tags and type-safe rules.","archived":false,"fork":false,"pushed_at":"2025-11-25T07:44:04.000Z","size":176,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-28T14:59:50.214Z","etag":null,"topics":["defaults","go","golang","golang-library","library","model","validation","validation-library"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ygrebnov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-04T07:49:27.000Z","updated_at":"2025-11-25T07:40:11.000Z","dependencies_parsed_at":"2025-09-08T08:28:02.550Z","dependency_job_id":"89d8af1a-6e03-4b76-a409-e3b9fae0cc9e","html_url":"https://github.com/ygrebnov/model","commit_stats":null,"previous_names":["ygrebnov/model"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/ygrebnov/model","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygrebnov%2Fmodel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygrebnov%2Fmodel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygrebnov%2Fmodel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygrebnov%2Fmodel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ygrebnov","download_url":"https://codeload.github.com/ygrebnov/model/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ygrebnov%2Fmodel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28480063,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T11:59:17.896Z","status":"ssl_error","status_checked_at":"2026-01-16T11:55:55.838Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["defaults","go","golang","golang-library","library","model","validation","validation-library"],"created_at":"2026-01-16T16:40:58.944Z","updated_at":"2026-01-16T16:40:59.060Z","avatar_url":"https://github.com/ygrebnov.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![GoDoc](https://pkg.go.dev/badge/github.com/ygrebnov/model)](https://pkg.go.dev/github.com/ygrebnov/model)\n[![Build Status](https://github.com/ygrebnov/model/actions/workflows/build.yml/badge.svg)](https://github.com/ygrebnov/model/actions/workflows/build.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/ygrebnov/model)](https://goreportcard.com/report/github.com/ygrebnov/model)\n\n# model — defaults \u0026 validation for Go structs\n\n`model` is a tiny helper that binds a **Model** (and optionally a reusable **Binding**) to your structs. It can:\n\n- **Set defaults** from struct tags like `default:\"…\"` and `defaultElem:\"…\"`.\n- **Validate** fields using named rules from `validate:\"…\"` and `validateElem:\"…\"`.\n- Accumulate all issues into a single **ValidationError** (no fail-fast).\n- Recurse through nested structs, pointers, slices/arrays, and map values.\n\nIt’s designed to be **small, explicit, and type-safe** (uses generics). You register rules (via `NewRule`) and `model` handles traversal, dispatch, and error reporting. Built‑in rules are always available implicitly (you don’t have to register them unless you want to override their behavior). For reusable validation across many values of the same type, you can use `Binding[T]` as a shared engine for defaults and validation.\n\n## Table of Contents\n- [Install](#install)\n- [Why use this?](#why-use-this)\n- [Quick start](#quick-start)\n- [Binding[T] – reusable defaults and validation](#bindingt--reusable-defaults-and-validation)\n- [Constructor: `New`](#constructor-new)\n- [Why no MustNew?](#why-no-mustnew)\n- [Functional options](#functional-options)\n- [Model methods](#model-methods)\n- [Struct tags (how it works)](#struct-tags-how-it-works)\n- [Built-in rules](#built-in-rules)\n- [Structured errors: errorc, sentinels, and ErrorField* keys](#structured-errors-errorc-sentinels-and-errorfield-keys)\n- [Overriding a builtin rule](#overriding-a-builtin-rule)\n- [Custom rules (with parameters)](#custom-rules-with-parameters)\n- [Error types](#error-types)\n- [Performance \u0026 benchmarks](#performance--benchmarks)\n  - [Performance tuning tips](#performance-tuning-tips)\n- [Behavior notes](#behavior-notes)\n- [Integration example: validation failure with sorted available types](#integration-example-validation-failure-with-sorted-available-types)\n- [Missing rule vs missing overload](#missing-rule-vs-missing-overload)\n- [Minimal example](#minimal-example)\n- [Examples](#examples)\n- [License](#license)\n\n---\n\n## Install\n\n```bash\ngo get github.com/ygrebnov/model\n```\n\n---\n\n## Why use this?\n\n- **Simple API**: one constructor and two main methods on `Model[T]`: `SetDefaults()` and `Validate(ctx)`. For reusable engines, use `Binding[T]` to apply the same defaults/validation to many instances.\n- **Predictable behavior**: defaults fill *only zero values*; validation gathers *all* issues.\n- **Extensible**: register your own rules; supports interface-based rules (e.g., rules for `fmt.Stringer`).\n- **Structured errors**: built-in rules and many internal errors use sentinel values plus structured key/value metadata (via `errorc`), making it easier to inspect and transform validation failures.\n\n---\n\n## Quick start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"encoding/json\"\n    \"errors\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com/ygrebnov/model\"\n)\n\ntype Address struct {\n    City    string `default:\"Paris\"  validate:\"min(3),max(50)\"`\n    Country string `default:\"France\" validate:\"min(3),max(50)\"`\n}\n\ntype User struct {\n    Name     string        `default:\"Anonymous\" validate:\"min(3),max(50)\"`\n    Age      int           `default:\"18\"        validate:\"min(1),nonzero\"`\n    Timeout  time.Duration `default:\"1s\"`\n    Home     Address       `default:\"dive\"`           // recurse into nested struct\n    Aliases  []string      `validateElem:\"min(3)\"`   // validate each element\n    Profiles map[string]Address `default:\"alloc\" defaultElem:\"dive\"`\n}\n\nfunc main() {\n    u := User{Aliases: []string{\"\", \"ok\"}} // index 0 will fail validation\n\n    m, err := model.New(\u0026u,\n        model.WithDefaults[User](),                       // apply defaults during construction\n        model.WithValidation[User](context.Background()), // run validation during construction (cancellable)\n    )\n    if err != nil {\n        var ve *model.ValidationError\n        if errors.As(err, \u0026ve) {\n            b, _ := json.MarshalIndent(ve, \"\", \"  \")\n            fmt.Println(string(b))\n        } else {\n            fmt.Println(\"error:\", err)\n        }\n        return\n    }\n\n    fmt.Printf(\"User after defaults: %+v\\n\", u)\n\n    // You can also call them later:\n    _ = m.SetDefaults()                  // guarded by sync.Once — no double work\n    _ = m.Validate(context.Background()) // returns *ValidationError on failure\n}\n```\n\n---\n\n## Binding[T] – reusable defaults and validation\n\n`Model[T]` binds a single struct instance; sometimes you want a reusable engine for a type that you can apply to many instances. That is what `Binding[T]` is for.\n\n```go\nimport (\n    \"context\"\n\n    \"github.com/ygrebnov/model\"\n)\n\ntype Payload struct {\n    ID      string `validate:\"uuid\"`\n    Email   string `validate:\"email\"`\n    Retries int    `validate:\"min(0),max(5)\"`\n}\n\nfunc validatePayload(ctx context.Context, p *Payload) error {\n    // Construct once per process (or cache it) and reuse.\n    b, err := model.NewBinding[Payload]()\n    if err != nil {\n        return err\n    }\n    // If you have custom rules:\n    //   customRule, _ := model.NewRule[Payload](...)\n    //   _ = b.RegisterRules(customRule)\n\n    // Apply defaults (from `default` tags) and then validate.\n    if err := b.ValidateWithDefaults(ctx, p); err != nil {\n        return err\n    }\n    return nil\n}\n```\n\nKey points:\n\n- `NewBinding[T]` builds a reusable binding for the type `T` using a fresh `RulesRegistry` and `RulesMapping`.\n- `Binding[T].ApplyDefaults(*T)` applies `default` / `defaultElem` tags to a concrete instance.\n- `Binding[T].Validate(ctx, *T)` validates a concrete instance using `validate` / `validateElem` tags.\n- `Binding[T].ValidateWithDefaults(ctx, *T)` combines both in a single call.\n- `Binding[T].RegisterRules(...)` lets you register custom validation rules for that binding’s type; these participate alongside the built-ins.\n\nA typical service pattern is to construct a `Binding[T]` once at startup and reuse it in handlers:\n\n```go\nvar payloadBinding *model.Binding[Payload]\n\nfunc init() {\n    var err error\n    payloadBinding, err = model.NewBinding[Payload]()\n    if err != nil {\n        panic(err) // or return a startup error from main\n    }\n}\n\nfunc handleRequest(ctx context.Context, p *Payload) error {\n    if err := payloadBinding.ValidateWithDefaults(ctx, p); err != nil {\n        return err\n    }\n    // p is now defaulted and validated\n    return nil\n}\n```\n\n---\n\n## Constructor: `New`\n\n```go\nctx := context.Background()\nm, err := model.New(\u0026user,\n    model.WithDefaults[User](),\n    model.WithValidation[User](ctx),  // run validation during New() with cancellation support\n)\nif err != nil {\n    var ve *model.ValidationError\n    switch {\n    case errors.Is(err, model.ErrNilObject):\n        // handle nil object\n    case errors.Is(err, model.ErrNotStructPtr):\n        // handle pointer to non-struct\n    case errors.As(err, \u0026ve):\n        // handle field validation failures\n    default:\n        // defaults parsing or other errors\n    }\n}\n```\n\nTo validate later explicitly, call `m.Validate(ctx)` with a context appropriate for the request.\n\n---\n\n## Why no MustNew?\n\n`MustNew` (a variant that panics on configuration errors) is intentionally omitted:\n\n- Panics hinder graceful startup error reporting in services / CLIs.\n- All failure modes (`nil` object, non-struct pointer, duplicate rule overload, validation failures when requested) are ordinary and recoverable.\n- Returning `error` keeps initialization explicit and test-friendly (you can assert exact sentinel errors or unwrap `*ValidationError`).\n- If you truly want a panic wrapper, you can write a 2‑line helper in your own code:\n  ```go\n  func MustNew[T any](o *T, opts ...model.Option[T]) *model.Model[T] {\n      m, err := model.New(o, opts...); if err != nil { panic(err) }; return m\n  }\n  ```\n\nIf enough users request it, a helper can be added later—keeping the core API minimal for now.\n\n---\n\n## Functional options\n\nAll options run in the order provided. If an option returns an error (e.g., attempting to register a duplicate overload for the same type \u0026 name), `New` stops and returns that error.\n\n### `WithDefaults[T]()` — apply defaults during construction\n\n```go\nm, err := model.New(\u0026u, model.WithDefaults[User]())\n```\n\n- Runs once per `Model` (guarded by `sync.Once`).\n- Writes only zero values.\n\n### `WithValidation[T](ctx context.Context)` — run validation during construction\n\n```go\nctx := context.Background()\nm, err := model.New(\u0026u,\n    model.WithValidation[User](ctx),\n)\n```\n\n- Gathers **all** field errors; returns a `*ValidationError` on failure.\n- Built-ins are always considered first for matching types.\n- Cancellation/deadlines follow the provided context.\n- To override a built-in rule, register a custom rule *before* `WithValidation`:\n\n```go\nminCustom, _ := model.NewRule[string](\"min\", func(s string, _ ...string) error {\n    if strings.TrimSpace(s) == \"\" { return fmt.Errorf(\"must not be blank or whitespace\") }\n    return nil\n})\n\nm, err := model.New(\u0026u,\n    model.WithRules[User](minCustom), // override\n    model.WithValidation[User](ctx),\n)\n```\n\n### `WithRules[T](rules ...Rule)` — register one or many rules\n\nCreate rules with `NewRule` and pass them. You can supply multiple different rule names and/or multiple overloads (different field types) in a single call. Duplicate exact overloads (same rule name \u0026 identical field type) are rejected.\n\n```go\nmaxLen, _ := model.NewRule[string](\"maxLen\", func(s string, params ...string) error {\n    if len(params) \u003c 1 { return fmt.Errorf(\"maxLen requires 1 param\") }\n    n, _ := strconv.Atoi(params[0])\n    if len(s) \u003e n { return fmt.Errorf(\"must be \u003c= %d chars\", n) }\n    return nil\n})\n\npositive64, _ := model.NewRule[int64](\"positive\", func(v int64, _ ...string) error {\n    if v \u003c= 0 { return fmt.Errorf(\"must be \u003e 0\") }\n    return nil\n})\n\nm, _ := model.New(\u0026u,\n    model.WithRules[User](maxLen, positive64), // different names \u0026 types allowed\n)\n```\n\nDuplicate exact overloads (same rule name \u0026 identical field type) are **rejected at registration time** with `ErrDuplicateOverloadRule`. This prevents later runtime ambiguity.\n\n---\n\n## Model methods\n\n### `SetDefaults() error`\n\nApply `default:\"…\"` / `defaultElem:\"…\"` recursively. Safe to call multiple times (subsequent calls no-op).\n\n### `Validate(ctx context.Context) error`\n\nWalk fields and apply rules from `validate:\"…\"` / `validateElem:\"…\"` tags. Returns `*ValidationError` on failure.\n\n- Returns `ctx.Err()` immediately if the context is canceled or its deadline is exceeded.\n\n---\n\n## Struct tags (how it works)\n\n### Defaults: `default:\"…\"` and `defaultElem:\"…\"`\n\n- **Literals**: string, bool, ints/uints, floats, `time.Duration`.\n- **`dive`**: recurse into struct or `*struct` (allocating a new struct for nil pointers).\n- **`alloc`**: allocate empty slice/map if nil.\n- **`defaultElem:\"dive\"`**: recurse into struct elements (slice/array) or map values.\n\nPointer-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.\n\n### Validation: `validate:\"…\"` and `validateElem:\"…\"`\n\n- Comma-separated top-level rules.\n- Parameters in parentheses: `rule(p1,p2)`.\n- Empty tokens skipped (`,email,` → `email`).\n- `validateElem:\"dive\"` recurses into struct elements; non-struct or nil pointer elements produce a misuse error under rule name `dive`.\n\n---\n\n## Built-in rules\n\nBuilt-in rules are always implicitly available (you do **not** need to register or import anything for them):\n\n- String:\n  - `min(N)` – length must be **\u003e= N** (N ≥ 1). If N \u003c 1, the rule is a no-op.\n  - `max(N)` – length must be **\u003c= N** (N ≥ 0). If N \u003c 0, the rule is a no-op.\n  - `oneof(v1,v2,...)` – value must be exactly one of the listed strings.\n  - `email` – lightweight email check (single `@`, non-empty local/domain, domain contains `.`, no whitespace).\n  - `uuid` – canonical UUID string (`8-4-4-4-12` hex format with hyphens).\n- Int / Int64 / Float64:\n  - `min(V)` – value must be **\u003e= V**.\n  - `max(V)` – value must be **\u003c= V**.\n  - `nonzero` – value must not be zero.\n  - `oneof(v1,v2,...)` – value must be equal to one of the listed values.\n\nOverriding: 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 \u0026 type are rejected). Interface-based rules still participate via assignable matching when no exact rule exists.\n\n\u003e The library lazy-loads built-ins on first use, so unused numeric/string sets impose no startup cost.\n\n---\n\n### Numeric min / max examples\n\n```go\ntype Limits struct {\n    Port    int     `validate:\"min(1),max(65535)\"`\n    Retries int64   `validate:\"min(0),max(10)\"`\n    Ratio   float64 `validate:\"min(0.0),max(1.0)\"`\n}\n```\n\n- `Port` must be between 1 and 65535.\n- `Retries` must be between 0 and 10.\n- `Ratio` must be between 0.0 and 1.0 inclusive.\n\n### String max and uuid examples\n\n```go\ntype Account struct {\n    ID    string `validate:\"uuid\"`\n    Name  string `validate:\"min(3),max(100)\"`\n    Email string `validate:\"email\"`\n}\n```\n\n- `ID` must be a canonical UUID string (e.g. `123e4567-e89b-12d3-a456-426614174000`).\n- `Name` must be between 3 and 100 characters.\n- `Email` must satisfy a simple email heuristic (single `@`, etc.).\n\n---\n\n## Structured errors: errorc, sentinels, and ErrorField* keys\n\nUnder the hood, `model` uses [`github.com/ygrebnov/errorc`](https://github.com/ygrebnov/errorc) to build structured errors. The `errors` package defines sentinel errors and strongly-typed keys:\n\n- Sentinels (examples):\n  - `errors.ErrNilObject`\n  - `errors.ErrNotStructPtr`\n  - `errors.ErrRuleMissingParameter`\n  - `errors.ErrRuleInvalidParameter`\n  - `errors.ErrRuleConstraintViolated`\n- Keys (examples):\n  - `errors.ErrorFieldRuleName` (e.g. `model.rule.name`)\n  - `errors.ErrorFieldRuleParamName` (e.g. `model.rule.param_name`)\n  - `errors.ErrorFieldRuleParamValue` (e.g. `model.rule.param_value`)\n  - `errors.ErrorFieldFieldName` (e.g. `model.field.name`)\n  - `errors.ErrorFieldCause` (the underlying cause error)\n\nBuiltin rules attach metadata when they fail. For example, the string `min` rule:\n\n```go\nreturn errorc.With(\n    errors.ErrRuleConstraintViolated,\n    errorc.String(errors.ErrorFieldRuleName, \"min\"),\n    errorc.String(errors.ErrorFieldRuleParamName, \"length\"),\n    errorc.String(errors.ErrorFieldRuleParamValue, raw),\n)\n```\n\nFrom your code, you can inspect these errors using `errors.Is` and by reading the message (which includes the structured key/value pairs), or by using `errors.As` into `*validation.Error` for field-level failures.\n\nExample:\n\n```go\nm, err := model.New(\u0026u, model.WithValidation[User](ctx))\nif err != nil {\n    var ve *model.ValidationError\n    if errors.As(err, \u0026ve) {\n        // Per-field errors\n        for path, fes := range ve.ByField() {\n            for _, fe := range fes {\n                fmt.Printf(\"field=%s rule=%s err=%v\\n\", path, fe.Rule, fe.Err)\n            }\n        }\n    }\n}\n```\n\nIf you need to work directly with the structured error metadata (e.g., to localize messages), you can call into `errorc` from your own code, or build small helpers around the keys exposed by `github.com/ygrebnov/model/errors`.\n\n---\n\n## Overriding a builtin rule\n\nYou can override a builtin rule by registering a custom rule with the same name and exact field type before validation runs. For example, to replace the builtin string `min` rule with a whitespace-aware version:\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"errors\"\n    \"fmt\"\n    \"strings\"\n\n    \"github.com/ygrebnov/model\"\n)\n\ntype Comment struct {\n    Text string `validate:\"min(3)\"`\n}\n\nfunc main() {\n    trimMin, err := model.NewRule[string](\"min\", func(s string, params ...string) error {\n        // Treat leading/trailing whitespace as insignificant.\n        s = strings.TrimSpace(s)\n        if len(params) == 0 {\n            return fmt.Errorf(\"min requires a length parameter\")\n        }\n        // For brevity, we skip full structured errorc usage here;\n        // in production, use sentinel errors + errorc.With, similar to builtin rules.\n        n := len(params[0]) // pretend this is parsed\n        if len(s) \u003c n {\n            return fmt.Errorf(\"must be at least %d characters after trimming\", n)\n        }\n        return nil\n    })\n    if err != nil {\n        panic(err)\n    }\n\n    c := Comment{Text: \"  x \"}\n\n    m, err := model.New(\u0026c,\n        model.WithRules[Comment](trimMin),           // override builtin string min\n        model.WithValidation[Comment](context.Background()),\n    )\n    if err != nil {\n        var ve *model.ValidationError\n        if errors.As(err, \u0026ve) {\n            fmt.Println(\"validation error:\", ve)\n        } else {\n            fmt.Println(\"error:\", err)\n        }\n        return\n    }\n\n    _ = m\n    fmt.Println(\"comment is valid\")\n}\n```\n\nIn this example, tag `validate:\"min(3)\"` for `Comment.Text` uses the overridden rule because it shares the same name and exact field type (`string`) as the builtin.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fygrebnov%2Fmodel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fygrebnov%2Fmodel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fygrebnov%2Fmodel/lists"}