AnnouncementsIntroducing Arkoren

    Menu

    Custom Rules

    Add application-specific value and object checks while preserving operational errors.

    What's on this page
    1. Return stable violation codes from typed custom checks.
    2. Keep operational failures distinct and avoid costly checks after invalid input.
    3. Check object invariants at the correct preparation stage.
    On this page

    A username can satisfy every format rule and still be reserved by your application. Add that policy with Check, whose callback receives the prepared value and caller’s context. Use built-in rules when they already express the constraint; custom checks are for application logic, not a replacement schema implementation.

    Examples import arkoren.dev/foundation/validate and the standard context package. The overview covers schema execution.

    Check a Value

    Return nil to accept a value or validate.Fail to report a violation:

    username := validate.String(func(name *validate.StringBuilder[string]) {
    	name.Trim()
    	name.Required()
    	name.Check(func(_ context.Context, value string) error {
    		if value == "admin" {
    			return validate.Fail("username.reserved", nil)
    		}
    		return nil
    	})
    })

    " admin " fails at the root pointer with code username.reserved after trimming. Add that code to your message catalog. Optional absent scalars skip ordinary checks; Required above handles absence.

    Fail accepts a parameter map for message placeholders. Parameters need to be JSON-serializable if you encode diagnostics; the helper does not encode or validate them. Do not include secrets merely to improve an error message.

    Every other returned error is operational: validation stops and returns it unchanged. If a storage lookup fails, return that failure rather than reporting that the username is unavailable. A failed dependency does not establish a fact about user input. Pass the callback’s context to storage and network calls.

    Stop Failed Checks

    Put cheap syntax rules before an expensive custom check and configure Bail when a malformed value must not reach that check. Inside the same typed builder, name.Bail() stops this value after its first violation.

    Rules otherwise collect violations in declaration order. Bail is a setting, not a step whose position changes its effect; it does not stop unrelated sibling fields. An operational error stops the whole validation regardless of Bail.

    Object Invariants

    For a relationship involving multiple fields, a struct-level check can select the field that needs correction. Define a booking interval:

    type Interval struct {
    	Start int `json:"start"`
    	End   int `json:"end"`
    }

    Attach a failed ordering check to the Go field name End:

    interval := validate.Struct(func(object *validate.StructBuilder[Interval]) {
    	object.Check(func(_ context.Context, value Interval) error {
    		if value.End < value.Start {
    			return validate.At("End", "interval.order", nil)
    		}
    		return nil
    	})
    })

    The diagnostic uses /end. At takes a Go field name, not a JSON Pointer; an unknown name produces an error wrapping ErrInvalidSchema during execution. For this simple comparison, GteField("Start") on an end-field builder is the smaller built-in alternative. Use an object check when the invariant needs broader application logic.

    An object check sees unconditional normalization and defaults, but runs before its child field rules. It does not see their later When or ExcludeIf effects and is not a final post-validation hook. Keep the invariant independent of those effects, and do not assume a child has already passed its own constraints.

    Context and Ownership

    Checks run for every applicable input, unlike construction callbacks. Published schemas support concurrent calls, so checks and captured dependencies must be safe for concurrent use. Do not mutate the input or objects reachable from it. Validation is not authorization, and checking a value does not make a later database mutation atomic with that check.

    Cancellation is observed between normalizers, rules, and child callbacks. Pass the context through operations that can block; the engine cannot interrupt an arbitrary application callback. Cancellation discards partial diagnostics and parsed results. An operational error that already aborted execution retains precedence over cancellation.

    Fail and At errors can be reused. They snapshot supported parameter containers, and the engine detaches them again when publishing violations. Pointers and opaque objects remain shared; follow the full diagnostic ownership rules rather than assuming a general deep copy.

    For all custom-check contracts, see the validate API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive