Validation
A constraint is a rule an answer has to satisfy. Constraints live in a
validate block inside the field:
form applicant version = "1.0" {
field age {
type = integer
label = "Age"
validate {
required
min = 18
max = 120
}
}
}
The constraints
| Constraint | What it means |
|---|---|
required |
the field must have been answered |
accepted |
a boolean must be true |
min / max |
the value is at least / at most this |
minLength / maxLength |
the text is this long at least / at most |
pattern |
the text matches this regular expression |
email / phone / url |
the text is an address of that kind |
unique |
no other field at that path holds the same value |
in / notIn |
the value is in / not in that list |
multipleOf |
the number divides by this |
equal / notEqual |
the same as / different from another field |
cross |
another field, with a message of its own |
custom |
a rule you write |
form applicant version = "1.0" {
field email {
type = string
validate { required; email }
}
field postcode {
type = string
validate { required; pattern = "^[A-Z]{2}[0-9]{2}$" }
}
field country {
type = string
validate { required; in = ["GB", "IE", "FR", "DE"] }
}
field start_date {
type = date
validate { required; notEqual = end_date }
}
}
required is not "must be true"
This is worth being careful with. A boolean field that has never been ticked
holds empty, and one that has been unticked holds false. false is a
value, not an absence.
So required on a boolean means "must have been assigned" — and false
satisfies it. A form whose consent must be affirmative has to say so:
form applicant version = "1.0" {
field consent {
type = boolean
label = "The applicant agrees to a credit check"
validate { accepted }
}
}
accepted fails unless the content is exactly true. An unassigned boolean,
an empty one and a false one all fail it.
The reference implementation also warns about this at compile time: required
on a boolean is W-VAL-01, whose message suggests accepted.
A constraint that only sometimes applies
Every constraint can carry a when guard:
form applicant version = "1.0" {
group user {
field age_group { type = string }
}
field age {
type = integer
validate {
required when user.age_group == "adult"
min = 18 when user.age_group == "adult"
}
}
}
A constraint whose when is false does not run. If the guard is unknown, the
constraint does not run either — the same three-valued rule as everywhere else.
custom — the escape hatch
custom takes an expression. The expression returns true or null when the
answer is valid, and a string when it is not. That string is the message.
form applicant version = "1.0" {
field birthdate {
type = date
label = "Date of birth"
validate {
required
custom = age(self) is between 18 and 75
? true
: t("applicant.too_young_or_old")
}
}
}
self is the field's own value. It is the subject of the constraint, so a
custom rule never has to name the field it is on.
The message is a string, not a key — but a key looked up with t() is a
string, and that is how a custom message stays translatable.
warn — a rule that only complains
Every constraint can be prefixed with warn, which makes a failure a warning
rather than an error:
form applicant version = "1.0" {
field age {
type = integer
validate {
warn min = 18
}
}
}
The form still commits; the person still sees the complaint.
When validation runs
Validation runs after the answers are in, not while they are typed. The runtime recomputes the whole form when anything changes, and the validity of the form is part of what it recomputes.
That is why a computed value can be validated: the value is computed first, and the constraint sees the computed value.
Every failure is reported
Constraints accumulate. If three constraints on one field fail, all three are reported — so a person can fix everything in one pass rather than one error at a time.
When the form cannot be satisfied
Some forms can never be valid. A field with min = 18 and max = 10 has no
answers at all, and the reference implementation says so when the document is
compiled, with a code that says which of the two bounds is wrong.
That is the satisfiability analysis: the runtime works out, from the types and the constraints, whether an answer exists at all. It is the difference between a form that asks a question nobody can answer and a form that merely has not been filled in yet.