AnnouncementsIntroducing Arkoren

    Menu

    Overview

    Define reusable rules for typed input and retain prepared values with Parse.

    What's on this page
    1. Construct a typed schema once and reuse it across input boundaries.
    2. Choose presence rules and defaults without confusing omission with zero.
    3. Keep parsed values only after validation succeeds.
    On this page

    A decoded request can have the correct Go type but still contain an empty email or an unsupported plan. A validation schema defines which values your application accepts. It can also prepare a cleaned, defaulted copy of the input. Use the same schema for requests, configuration, CLI input, or imported records.

    Install the standalone module from your application’s module directory:

    go get arkoren.dev/foundation/validate

    Import arkoren.dev/foundation/validate as validate. Examples use an existing context.Context named ctx. Focused fragments show error results without repeating routine propagation; application code must check them before using parsed input.

    Build a Schema

    Start with the type your application already decodes. JSON tags determine diagnostic names; schema registration uses Go field names.

    type Signup struct {
    	Email string `json:"email"`
    	Plan  string `json:"plan"`
    }

    Configure the accepted input inside Struct. Each callback receives a temporary, typed builder, not the published schema:

    signup := validate.Struct(func(object *validate.StructBuilder[Signup]) {
    	object.Field("Email", func(address *validate.StringBuilder[string]) {
    		address.Trim()
    		address.Required()
    		address.Email()
    	})
    	object.Field("Plan", func(plan *validate.StringBuilder[string]) {
    		plan.Default("free")
    		plan.OneOf("free", "pro")
    	})
    })

    Only registered fields are checked. Construction callbacks run once, synchronously, in argument order; there is no separate Build call. The published schema exposes Parse, Validate, and JSONSchema. For nested input and shared field policies, use Structs and Reuse.

    Parse Input

    Use Parse when the application needs the prepared value rather than the original input:

    input := Signup{Email: " ada@example.com "}
    parsed, err := signup.Parse(ctx, input)

    On success, parsed.Email is "ada@example.com" and parsed.Plan is "free". input is unchanged. On failure, Parse returns the zero result and an error, not a partially accepted form.

    If you only need a decision, call signup.Validate(ctx, input). It performs the same preparation and checks but discards the prepared value. Neither method decodes JSON or converts an incompatible Go type.

    Choose Presence

    Values are optional by default. Empty strings, zero numbers, false booleans, nil pointers, and nil slices or maps count as absent and skip ordinary rules. A zero time.Time is absent regardless of its location. Required rejects absence, rather than merely requiring a JSON key.

    For example, this schema rejects a missing quantity:

    quantity := validate.Int(func(count *validate.IntBuilder[int]) {
    	count.Required()
    	count.Min(1)
    })
    err := quantity.Validate(ctx, 0)

    Without Required, zero would skip Min(1) and pass. Use a pointer field when zero is a supplied value that must be checked. Non-nil pointees and collection entries are checked even when zero. An allocated empty slice or map is present: Required accepts it, while Min(1) rejects it. Nested value structs always check registered fields.

    Supply Defaults

    Use Default when absence should select an application policy rather than fail:

    limit := validate.Int(func(size *validate.IntBuilder[int]) {
    	size.Default(20)
    	size.Between(1, 100)
    })
    size, err := limit.Parse(ctx, 0)

    On success, size is 20. Defaults must satisfy their rules and must already be in their final form: normalizers run before defaults, not again afterward. Required and Default cannot coexist on one definition. FileBuilder has no default setting. Slice and map defaults copy their outer containers, not arbitrary nested objects; pointer defaults take a pointee value. See Normalizers for preparation order.

    Share Schemas

    Construct stable definitions once and share their published values across goroutines. Publication detaches schema-owned rule containers; retained builders are not runtime configuration objects.

    This does not recursively clone arbitrary default objects or captured data. Keep shared maps, pointers, catalogs, and callback state immutable or synchronize access. Custom callbacks must not mutate data reachable from their input.

    Bad definitions are programming errors, not rejected user input. Struct aggregates recorded construction problems and panics with ErrInvalidSchema; invalid standalone explicit schemas can panic when executed. See Definition Errors before assembling dynamic rules.

    HTTP Input

    With request imported from arkoren.dev/core/http/request, an HTTP handler can decode and validate its body using the same schema:

    input, err := request.ParseJSON(r, signup)

    Successful input includes normalization and defaults. Validation failures become 422 responses. If you validate separately, convert failures with request.ValidationProblem(err); raw validate.Errors has no HTTP status and otherwise becomes a 500. Configure decoding and body limits through the request guide.

    Continue with Scalar Rules for constraints or Validation Errors for structured diagnostics. For all public 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