AnnouncementsIntroducing Arkoren

    Menu

    Conditions

    Select field requirements and scoped rules from prepared values in the enclosing struct.

    What's on this page
    1. Require, prohibit, exclude, or further constrain a field conditionally.
    2. Combine observations using Go field paths and exact-type predicates.
    3. Account for missing paths, empty collections, and conditional effect order.
    On this page

    A delivery form needs an address for shipping, but not for pickup. A condition observes input without changing it or producing a violation. The field builder chooses the effect: require, prohibit, exclude, or add rules. Conditions read the nearest enclosing struct, so start with a struct schema.

    Examples import arkoren.dev/foundation/validate. Construction callbacks run once; their recorded observations run for each input.

    Require a Field

    Define a delivery form whose address requirement depends on its mode:

    type Delivery struct {
    	Mode    string `json:"mode"`
    	Address string `json:"address"`
    	Email   string `json:"email"`
    	Phone   string `json:"phone"`
    }
    delivery := validate.Struct(func(object *validate.StructBuilder[Delivery]) {
    	object.Field("Address", func(address *validate.StringBuilder[string]) {
    		address.RequiredIf(func(condition *validate.ConditionBuilder) {
    			condition.Eq("Mode", "shipping")
    		})
    	})
    })

    An empty address fails for shipping. Otherwise it is optional, not prohibited. RequiredUnless reverses the condition. Multiple condition callbacks configure one fresh condition draft, with observations combined using AND.

    Presence Dependencies

    If contact details need at least one channel, configure the phone field with phone.RequiredWithout("Email"). Here phone is its *validate.StringBuilder[string] inside the Delivery declaration.

    Setting Requires this field when
    RequiredWith("Email", "Address") Any listed field is present
    RequiredWithAll("Email", "Address") Every listed field is present
    RequiredWithout("Email", "Address") Any listed field is absent

    These shortcuts take string paths, not condition callbacks. Presence follows typed rules: zero scalars and nil pointers/containers are absent; non-nil pointers to zero and allocated empty collections are present. Zero time is absent regardless of location. This is not JSON-key existence.

    Prohibit or Exclude

    For pickup, decide whether an entered address is a mistake or stale form state. To report it as a mistake, add this setting inside the address callback:

    address.ProhibitedIf(func(condition *validate.ConditionBuilder) {
    	condition.Eq("Mode", "pickup")
    })

    A supplied address now fails for pickup. ProhibitedUnless allows a supplied value only when its condition matches.

    If you deliberately want to discard stale state instead, use this alternative:

    address.ExcludeIf(func(condition *validate.ConditionBuilder) {
    	condition.Eq("Mode", "pickup")
    })

    Matching input skips the field’s later rules and Parse clears the value in the result. Exclusion is not rejection and does not modify the caller’s input. Do not use it when the presence of that data must be reported.

    Add Branch Rules

    Some modes need stronger constraints while retaining ordinary rules. Within the same string address callback, add a minimum only for shipping:

    address.Max(200)
    address.When(func(condition *validate.ConditionBuilder) {
    	condition.Eq("Mode", "shipping")
    }, func(text *validate.StringBuilder[string]) {
    	text.Min(10)
    })

    The maximum applies to every supplied address; the minimum applies only in the branch. When takes one condition callback followed by callbacks for the same concrete builder role. Supply settings, not a published schema. Branch callbacks configure the schema once and are not rerun per input.

    Combine Observations

    An explicit group expresses alternatives. For an approval field beside string fields Plan and Role, use an OR group:

    approval.RequiredIf(func(condition *validate.ConditionBuilder) {
    	condition.Or(func(group *validate.ConditionBuilder) {
    		group.Eq("Plan", "enterprise")
    		group.Eq("Role", "admin")
    	})
    })

    Here approval is its string field builder. Either comparison requires approval. Ungrouped observations use AND; And makes that grouping explicit, and Not complements its nested group. Empty conditions and empty explicit groups are definition errors.

    Eq and NotEq compare a selected field with an assignable value. In and NotIn compare against a captured value list. Present and Absent observe typed presence. For logic clearer in Go, configure a typed predicate. Within the delivery address callback, this requires an address for shipping when email is absent:

    address.RequiredIf(func(condition *validate.ConditionBuilder) {
    	condition.If(func(value Delivery) bool {
    		return value.Mode == "shipping" && value.Email == ""
    	})
    })

    The predicate argument must exactly match the nearest struct type. It is a pure observation: it cannot normalize data or return an operational error.

    Field Paths

    Paths use dot-separated Go names, such as "Billing.Country" for a field Billing *Address whose address has a string Country. They walk structs and pointers, including promoted fields, but cannot index collections, select parents, or use JSON tag names. Inside an address child schema, paths begin at that address rather than its outer form.

    Bad paths and incompatible comparison types are construction errors. A nil intermediate pointer is unreachable and is never allocated. Eq, NotEq, In, and NotIn all return false for an unreachable target. Use an explicit Not group if the missing case must count as the complement. Reachable scalar zeros are compared normally.

    Present is false for an unreachable path; Absent is true. Field assertions such as EqField use the same resolver but fail when the target is unreachable, rather than comparing an invented zero.

    Observe Collections

    To require approval when any team member is an administrator, assume the enclosing form has Members []Member, where Member contains Admin bool:

    approval.RequiredIf(func(condition *validate.ConditionBuilder) {
    	condition.Contains("Members", func(_ int, member Member) bool {
    		return member.Admin
    	})
    })

    Slice predicates take the exact (int, E) types; maps take (K, V). Their results observe the collection, not validate its entries:

    Observation Condition is true when
    Has, HasAny, HasAll A map has the selected key, any listed key, or all listed keys
    Contains At least one entry matches
    Every Every entry matches
    UniqueBy Comparable projected identities are unique
    CountWhere The actual matching count satisfies pure integer constraints

    Each takes the field path first. CountWhere uses the same restricted count rules as a collection assertion.

    A reachable nil collection is observed as empty, unlike an optional collection’s own assertions: Contains is false; Every and UniqueBy are true; count rules check zero. Nil maps lack every key. Empty key lists make HasAny false and HasAll true. An unreachable path instead returns false for all collection observations, including Every and HasAll.

    Effect Order

    Conditions see unconditional normalization and defaults in reachable validated children before field rules run, regardless of Go field order. A sibling with Trim is observed after trimming, not as raw text.

    Conditional When and ExcludeIf effects remain sensitive to declaration and field execution order. They do not repeatedly evaluate until every dependent field settles. Avoid mutually dependent branches that assume later effects have already happened. An object check sees unconditional preparation, but is not a final hook after conditional effects.

    Conditions and arbitrary predicates are runtime-only and omitted from JSON Schema. Keep predicates read-only and concurrency-safe.

    For exact settings and observations, 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