FS FormScript

Validation

A constraint is a rule an answer has to satisfy. Constraints live in a validate block inside the field:

forms
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
forms
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:

forms
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:

forms
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.

forms
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:

forms
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.