Skip to content
latest
Framework Packages / latest

Validation

github.com/mbvlabs/andurel/pkg/validation builds structured, JSON-ready field errors and records rule metadata in the same pass. It is plain Go: configuration and domain inputs use it without coupling validation to HTTP, Inertia, the database or the CLI.

Builder lifecycle

Create a builder, add rules, and return Err(). Every rule records its code, message and parameters even when the value passes; a failed rule also appends a field error.

1func (p *Product) Validate() error {
2    b := validation.NewBuilder()
3    b.Required("name", p.Name)
4    b.LenBetween("name", p.Name, 2, 120)
5    b.MinInt("priceCents", p.PriceCents, 0)
6    b.OneOf("status", p.Status, "draft", "active", "archived")
7
8    return b.Err()
9}

Err() returns nil or ValidationErrors. Errors() exposes the failures, and Rules() returns a copy of the client-usable rule metadata keyed by field. ToMap() keeps the first message for each field, which matches the usual form contract.

Rules fire in the order you add them, so add Required before format or range checks. Most rules also ignore an absent optional value, which makes the combination deliberate: Required when absence is invalid, the format rule when the value is present.

Built-in rules

Rule Behavior
Required, RequiredWhen Reject missing or zero-like values, conditionally when needed
MinLen, MaxLen, LenBetween Count Unicode code points in a present string
RecommendedLenBetween Publish recommendation metadata without creating an error
MinInt, MaxInt Bound present integers
OneOf, OneOfWithMessage Restrict a present string to allowed values
URL, RequiredURL Accept HTTP or HTTPS URLs, optionally requiring presence
Email Parse the value as a bare email address
MinItems, MaxItems Bound a present slice or array
NoBlankItems Report blank string elements as Field[i]
TimeBeforeOrEqual Compare two present time values
True Require a true boolean, for example an agreement checkbox

Messages may be customized per call; stable codes and parameters let a client localize or present richer guidance.

Presence and zero semantics

Presence helpers understand strings, integers, booleans, slices, maps, time.Time, pointers, the common database/sql nullable types, and defined types whose underlying kind is supported. A pointer, slice or map is missing when it is nil or empty; a string is missing when it is blank; booleans are missing when false; other values are missing when they are the zero value.

Be explicit about zero semantics. Required treats integer zero and zero time as missing, which is right for many identifiers but wrong when zero is legitimate input. Model that field as a pointer or a nullable type and use the domain's range rule. NoBlankItems reports indexed fields such as Tags[2]; an unnamed field is normalized to unknown.

Structured errors

Each failure carries field, code, message and optional params:

1{
2  "field": "name",
3  "code": "min",
4  "message": "must be at least 2 characters",
5  "params": {"min": 2}
6}

validation.As(err) extracts ValidationErrors through wrapped errors. Use it in a service or handler that needs to branch on field failures without depending on the builder.

Domain-specific rules

Use AddRule or AddRuleWithParams to describe a rule the client should know about, and AddField or AddFieldWithParams to record an actual failure:

1b.AddRuleWithParams("slug", "reserved", "is reserved", map[string]any{
2    "values": reservedSlugs,
3})
4if slices.Contains(reservedSlugs, data.Slug) {
5    b.AddField("slug", "reserved", "is reserved")
6}

Keep rules deterministic and side-effect free. Database uniqueness, authorization and race-sensitive invariants belong in a domain or service backed by constraints; translate those failures into the same field shape when a form needs them.

validation.Join combines several results into one ValidationErrors and preserves other errors, which is useful when one operation validates an entity and a nested input struct:

1if err := validation.Join(entity.Validate(), data.PasswordPair.Validate()); err != nil {
2    return User{}, err
3}

validation.Field(field, code, message) builds a single field error for a case that has no builder.

Presentation

The router's error handler already understands the type. inertia.ErrorHandler detects the ToMap() method: an Inertia request gets a 422 Errors/Validation page with the field messages in props.errors, and a JSON request gets 422 with a fields object.

When a handler renders the form itself, use the builder directly:

1if page, ok := renderer.Page(ctx, "Auth/Login", inertia.Props{}).Errors(err); ok {
2    return page.Render()
3}

Errors reports whether the error carried field errors; when it did not, fall through to the normal error path. ValidationErrors is the explicit form for a message you produced yourself.

Validation is not authorization. Authenticate the actor, authorize the operation and resource, validate input, then execute it. Never use field validation, or hidden request context, as a permission system.

Configuration validation

Generated constructors combine environment parsing errors with builder errors and semantic checks:

1cfg := storage.DefaultConfig()
2// … fill from the environment …
3
4if err := errors.Join(e.Err(), cfg.Validate()); err != nil {
5    return storage.Config{}, fmt.Errorf("config: database: %w", err)
6}

The builder covers required values and bounds; the package's Validate covers relationships such as supported modes, port ranges or timeout ordering. The joined error stops the process before a dependent component is constructed with an invalid value.