AnnouncementsIntroducing Arkoren

    Menu

    Normalizers

    Prepare cleaned values before checking presence and rules, without changing caller-owned input.

    What's on this page
    1. Choose deliberate transformations instead of rejecting the original value.
    2. Clean text, numbers, times, and lists in the parsed result.
    3. Account for child preparation, defaults, diagnostic indices, and callback ownership.
    On this page

    An imported tag such as " Go " may be usable if you store it as "go". A normalizer makes that deliberate replacement before validation. Use an assertion instead when the original spelling or range must be rejected.

    Parse returns the prepared value; Validate checks it and discards it. Neither modifies caller-owned input. Examples import arkoren.dev/foundation/validate and use an existing context named ctx.

    Clean Text

    Accept surrounding whitespace and mixed case for imported tags:

    tag := validate.String(func(text *validate.StringBuilder[string]) {
    	text.Trim()
    	text.ToLower()
    	text.Required()
    })
    parsed, err := tag.Parse(ctx, " Go ")

    On success, parsed is "go". Lowercase would instead reject uppercase letters without changing them.

    Setting Replacement
    Trim() Remove surrounding Unicode whitespace
    ToLower(), ToUpper() Change case
    TrimPrefix(prefix), TrimSuffix(suffix) Remove one matching prefix or suffix
    Truncate(n) Keep at most n runes

    These preserve the builder’s exact string type. Truncation does not report a length violation: use Max to reject long text. Zero truncates to empty; negative lengths are invalid definitions.

    Adjust Numbers

    For an import policy that intentionally caps percentages rather than rejecting out-of-range input, use Clamp:

    percentage := validate.Int(func(number *validate.IntBuilder[int]) {
    	number.Clamp(0, 100)
    })
    parsed, err := percentage.Parse(ctx, 120)

    The result is 100 on success. Max(100) would reject 120 instead. Signed, unsigned, and floating builders support inclusive Clamp; reversed bounds panic. Signed and floating builders also support Abs; the minimum signed integer stays unchanged because its positive counterpart cannot fit.

    Floating builders additionally support decimal rounding:

    measurement := validate.Float(func(number *validate.FloatBuilder[float64]) {
    	number.Round(2)
    })
    parsed, err := measurement.Parse(ctx, 12.345)

    The result is 12.35. Halves round away from zero; negative decimal places panic. Rounding changes the quantity and cannot restore precision already lost. Do not apply it when the original precision is part of the contract.

    Normalize Time

    To store an accepted timestamp with UTC location, import the standard time package for your input and configure Time:

    timestamp := validate.Time(func(instant *validate.TimeBuilder) {
    	instant.UTC()
    })

    Parsing changes its location to UTC without changing the instant or original value. Time assertions still compare instants.

    Clean Lists

    Normalize entries before removing empty tags and duplicates:

    tags := validate.Slice(func(items *validate.SliceBuilder[string]) {
    	items.Values(func(tag *validate.StringBuilder[string]) {
    		tag.Trim()
    		tag.ToLower()
    	})
    	items.RejectZero()
    	items.Dedupe()
    	items.Min(1)
    })
    parsed, err := tags.Parse(ctx, []string{" Go ", "", "go", "Queues"})

    The result is []string{"go", "queues"} on success. Entry cleanup happens first, so deduplication sees identical "go" values. Diagnostics use the cleaned slice’s indices, not original import-row positions.

    Sort takes func(E, E) int, such as cmp.Compare[int] from the standard cmp package. Filter keeps entries matching func(int, E) bool; Reject drops matches. RejectZero drops zero entries. Dedupe keeps the first occurrence and requires comparable, non-interface elements.

    Filtering is not Every, and deduplication is not Unique: the former change data while the latter report invalid collections. Maps have custom Normalize, but no built-in sort/filter/deduplication operations. Map keys never normalize, default, or exclude because that could change identity.

    Custom Replacement

    For an application-specific decoration, return a same-type replacement from Normalize. With the standard strings package imported:

    name := validate.String(func(text *validate.StringBuilder[string]) {
    	text.Normalize(func(input string) string {
    		return strings.TrimPrefix(input, "@")
    	})
    })

    Parsing "@ada" produces "ada". Every explicit rule builder supports typed Normalize, including structs, pointers, maps, files, text values, and dynamic values. Named-container adapters instead attach a configured source schema.

    There is no error return. Use a custom check if an operation can fail. Keep callbacks concurrency-safe and do not mutate nested maps, slices, or pointers reachable from the input: arbitrary captured/default objects remain application-owned, not recursively cloned.

    Preparation Order

    Unconditional normalizers run in their own declaration order before presence, defaults, and ordinary rules. Placing Trim after Required in the declaration does not make presence run first. Whitespace-only text becomes absent after trimming, so a requirement fails or a default applies.

    Defaults are not normalized again at the value where they are assigned. Supply them in final form. Nested children prepare before enclosing conditions; slice elements prepare before slice normalizers, and pointer pointees and map values likewise prepare before their enclosing checks. Each normalizer runs once along that path. Container assertions and child validation then see prepared values.

    Conditional transformations remain order-sensitive. Map-key rules, condition observations, and CountWhere count constraints cannot normalize or default values: they test identity or actual observations rather than rewrite the data deciding a rule.

    For exact transformation 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