The condition language is a small expression evaluator used by RequiredIf, RequiredUnless, When, and Unless to express cross-field dependencies. It is evaluated at validation time against the current input.
- Literals
- Field paths
- Comparison operators
- Logical operators
- Grouping
- Functions
- Type coercion
- Grammar reference
- Error conditions
- Examples
| Kind | Syntax | Go type produced |
|---|---|---|
| String | "hello" or 'hi' |
string |
| Integer | 42 |
int |
| Float | 3.14 |
float64 |
| Boolean | true / false |
bool |
String literals use either double or single quotes. Escape sequences are not supported; use the other quote style if the value contains a quote character.
role == "admin"
label == 'it\'s' // not valid — no escape support; use "it's" instead
label == "it's" // correct
Unquoted identifiers are resolved as dot-separated paths into the current input. Each segment navigates one level of nesting.
role // top-level field
order.status // nested: input["order"]["status"]
user.address.city // two levels deep
- A path that does not exist resolves to
nil. nilcompared to anything with==returnsfalse; with!=returnstrue.- Field names may contain letters, digits, and underscores.
| Operator | Meaning |
|---|---|
== |
equal |
!= |
not equal |
< |
less than |
> |
greater than |
<= |
less than or equal |
>= |
greater than or equal |
The right-hand side of a comparison can be a literal or another field path.
age >= 18
role == "admin"
role == expected_role // both sides resolved as field paths
score < passing_threshold
Ordering semantics — see Type coercion.
| Operator | Meaning | Precedence |
|---|---|---|
! |
NOT (unary) | highest |
&& |
AND (binary) | middle |
|| |
OR (binary) | lowest |
! applies to the immediately following atom or grouped expression. && binds tighter than ||, so a || b && c is parsed as a || (b && c).
!verified
role == "admin" && plan != "free"
status == "active" || status == "pending"
! requires its operand to be boolean. Applying ! to a non-boolean field is an error.
Parentheses override default precedence:
(status == "active" || status == "pending") && verified == true
!(role == "guest" || role == "banned")
Returns true if the field at path is present in the input, regardless of its value. Returns false when the field is absent.
exists(email)
exists(order.shipping_address)
Presence is distinct from truthiness — exists(field) returns true even when field is "", 0, false, or nil.
Returns the length of the value at path:
| Value type | Length returned |
|---|---|
string |
number of bytes |
| slice / array | number of elements |
| map | number of keys |
| absent / other | 0 |
len(tags) > 0
len(name) >= 3 && len(name) <= 50
len(order.items) == 0
len always returns an integer, so it can be compared to integer literals with any comparison operator.
Function argument must be a field path. String literals and other expressions are not accepted as function arguments.
exists(email) // correct
exists("email") // error: argument must be a field path
len(tags) // correct
len("tags") // error: argument must be a field path
When both sides of a comparison resolve to a numeric Go type (int, int8, …, uint64, float32, float64), all values are promoted to float64 and compared numerically.
age > 17 // numeric: input age=18 → 18.0 > 17.0 → true
score >= 4.5 // numeric: input score=5.0 → 5.0 >= 4.5 → true
count != 0 // numeric: input count=1 → 1.0 != 0.0 → true
When either side is not numeric (e.g. a string field or literal), both sides are converted to strings via fmt.Sprintf("%v", v) and compared lexicographically.
name < "z" // lexicographic: "alice" < "z" → true
status == "active" // string equality
Boolean fields returned from the input are compared directly as strings unless used with == / != against a bool literal, in which case the comparison uses the string representation ("true" / "false").
To test a boolean field, prefer:
verified == true
verified == false
expr = or
or = and { "||" and }
and = cmp { "&&" cmp }
cmp = unary [ op unary ]
unary = "!" atom | atom
atom = "(" expr ")" | call | ident | string | int | float | bool
call = name "(" ident ")"
op = "==" | "!=" | "<" | ">" | "<=" | ">="
ident = letter { letter | digit | "_" | "." }
string = '"' { char } '"' | "'" { char } "'"
int = digit { digit }
float = digit { digit } "." digit { digit }
bool = "true" | "false"
Whitespace between tokens is ignored. Unknown characters are skipped silently.
The evaluator returns an error (surfaced as a RuleSyntaxError from schema.Validate) for:
| Situation | Example |
|---|---|
| Syntax error / unexpected token | role == (missing right-hand side) |
| Unclosed parenthesis | (role == "admin" |
! applied to a non-boolean |
!role where role is a string |
| Unknown function | missing(field) |
| Function argument is not a field path | exists("email") |
| Invalid numeric literal | 3.14.15 |
validation.New().
Field("vat_number", validation.RequiredIf(`plan == "paid"`))validation.New().
Field("admin_code", validation.RequiredIf(`role == "admin" && exists(org_id)`))validation.New().
Field("note", validation.RequiredIf(`(status == "active" || status == "pending") && verified == true`))validation.New().
Field("guardian_name", validation.RequiredIf(`age < 18`))validation.New().
Field("tag_description", validation.RequiredIf(`len(tags) > 0`))validation.New().
Field("billing_zip", validation.RequiredIf(`order.type == "physical"`))validation.New().
Field("confirm_password", validation.RequiredIf(`password != confirm_password`))validation.New().
Field("reason", validation.RequiredUnless(`status == "approved"`))validation.New().
Field("email", validation.When(`role == "admin"`, validation.Email, validation.Required))