Rules reject input; they do not repair it. Choose them when an invalid value
must produce a diagnostic rather than a silently changed result. The
overview establishes construction and execution; examples import
arkoren.dev/foundation/validate and use an existing context named ctx.
Absent scalar values skip ordinary rules. Add Required to reject absence, or
use a pointer when an explicit zero
must be checked. String, Int, Uint, Float, and Bool preserve named Go
types as well as built-in types. Without a callback to infer the type, write
validate.String[string]() or validate.Int[int64]().
Text Constraints
For a public display name, reject blank or overlong text without changing its spelling:
name := validate.String(func(text *validate.StringBuilder[string]) {
text.Required()
text.NotBlank()
text.Max(80)
})
err := name.Validate(ctx, "Ada Lovelace")
This passes. Lengths count Unicode runes, not bytes. NotBlank rejects
whitespace-only text but does not trim it; normalizers own
deliberate transformations.
| Rule | Accepted supplied text |
|---|---|
Min, Max, Len |
Minimum, maximum, or exact nonnegative rune count |
NotBlank |
At least one non-whitespace character |
OneOf, NotOneOf |
Membership or nonmembership in the supplied typed values |
Regex |
Matches a supplied *regexp.Regexp; anchor it to match the whole string |
Prefix, Suffix, Contains |
Has the supplied prefix, suffix, or substring |
Lowercase, Uppercase |
Contains no uppercase or no lowercase letters; no rewriting |
Alpha, AlphaNumeric, AlphaDash |
ASCII letters; letters/digits; or letters/digits/hyphens/underscores |
ASCII |
ASCII code points only |
For an application plan, plan.OneOf("free", "pro") inside its string callback
accepts only those supplied values. There is no separate enum constructor.
NotOneOf("admin", "root") excludes reserved names. Negative length bounds
record definition errors, not input violations.
String Formats
A callback URL should use a scheme your application intends to handle:
callback := validate.String(func(address *validate.StringBuilder[string]) {
address.Required()
address.URL("https")
})
err := callback.Validate(ctx, "https://example.com/events")
This passes. URL requires an absolute URL with a host. Its default schemes are
HTTP and HTTPS, compared case-insensitively. An explicit list replaces those
defaults and is copied when the setting is declared. An empty list selects the
defaults; an empty string entry never permits relative or scheme-relative URLs.
Syntax validation does not authorize a network destination: apply your own
destination policy before fetching an untrusted URL.
| Rule | What it checks |
|---|---|
Email |
Email syntax, not mailbox existence |
URL |
Absolute URL, host, and allowed scheme |
UUID |
UUID text |
Domain |
Domain name, not an entire URL |
IP |
IPv4 or IPv6 text |
Duration |
Go duration text accepted by time.ParseDuration |
These checks do not trim whitespace. Add Trim only when accepting and returning
cleaned text is intentional.
Numeric Bounds
Use integral units when a quantity is naturally a count. A batch size can be bounded without floating-point calculations:
batch := validate.Int(func(count *validate.IntBuilder[int]) {
count.Required()
count.Between(1, 100)
count.MultipleOf(5)
})
err := batch.Validate(ctx, 25)
This passes. Arguments use the schema’s exact numeric type. Int, Uint, and
Float share these assertions:
| Rule | Supplied number must be |
|---|---|
Min, Gte |
Greater than or equal to the bound |
Max, Lte |
Less than or equal to the bound |
Between |
Inside both inclusive bounds |
Gt, Lt |
Strictly greater or strictly less |
Positive, NonNegative |
Greater than zero or greater than/equal to zero |
MultipleOf |
An exact multiple of a positive factor |
OneOf, NotOneOf |
In or outside the listed typed values |
MultipleOf(0) panics immediately. Negative factors and non-finite floating
bounds or factors record sticky definition errors. Floating MultipleOf compares
exact decimal values from each Go float’s shortest JSON spelling: 0.3 passes a
factor of 0.1, without epsilon tolerance. Float32 retains its own encoding
precision. This cannot recover precision lost during an earlier calculation.
Field Comparisons
To check confirmation input, compare the field with another exact-type field in the nearest enclosing struct:
type Passwords struct {
Password string `json:"password"`
Confirm string `json:"confirm"`
}
passwords := validate.Struct(func(object *validate.StructBuilder[Passwords]) {
object.Field("Confirm", func(text *validate.StringBuilder[string]) {
text.Required()
text.EqField("Password")
})
})
A supplied confirmation must match Password. NeField checks inequality;
numeric builders additionally have GtField, GteField, LtField, and
LteField. Paths use Go names, not JSON names, and an unreachable target fails
the comparison. See Field Paths for nested scope.
Boolean Intent
Agreement needs an affirmative value, not merely a non-nil field:
terms := validate.Bool(func(consent *validate.BoolBuilder[bool]) {
consent.Accepted()
})
err := terms.Validate(ctx, false)
This fails: Accepted requires true even though false normally counts as absent.
Declined rejects true. To require an explicitly supplied false, require a
pointer and apply Declined to its pointee.
Time Windows
Time validates time.Time. After and Before are strict; Between includes
both bounds. AfterField and BeforeField compare other exact-type time fields.
For a future appointment, pin the clock in tests rather than sleeping:
now := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
fixed := validate.NewClockContext(ctx, func() time.Time {
return now
})
appointment := validate.Time(func(instant *validate.TimeBuilder) {
instant.Required()
instant.Future()
})
err := appointment.Validate(fixed, now.Add(time.Hour))
With the standard time package imported, this passes. Equality with the clock
fails both Future and Past. A zero instant is absent in every location.
Duration Values
If your field remains a string, use String with Duration(). If your decoded
type needs a duration value that also passes through JSON, use
validate.Duration and Text[validate.Duration]():
var timeout validate.Duration
err := timeout.UnmarshalText([]byte("250ms"))
duration := timeout.Std()
text, err := timeout.MarshalText()
On success, duration is a standard time.Duration of 250 milliseconds and
text is []byte("250ms"). Failed unmarshaling leaves the receiver unchanged.
Raw time.Duration has no representation in encoding/json/v2.
The wrapper’s JSONSchemaFormat() returns "go-duration"; standard JSON Schema
"duration" uses a different grammar. Text[T] requires text marshaling and
unmarshaling on *T; it and Any offer common settings and custom checks,
not string-format methods.
File Uploads
After multipart decoding, constrain the *multipart.FileHeader itself:
avatar := validate.File(func(upload *validate.FileBuilder) {
upload.Required()
upload.Max(2 << 20)
upload.Extensions("png")
upload.Types("image/png")
})
Nil fails Required. Min and Max use the multipart parser’s size in bytes.
Extensions ignores case and an optional leading dot. Types opens the file,
checks its first 512 bytes using http.DetectContentType, and accepts exact
types or wildcards with media-type parameters ignored. Open/read failures are
operational errors, not invalid-input violations.
Content detection is coarse: Office files detect as ZIP, and SVG can contain
scripts and is not treated as an image. There is no Image shorthand. Keep
antivirus checks, safe storage names, and
multipart body limits; these rules
do not replace them.
For every scalar method, see the
validate API reference.