Validators
The full validator catalog — standard, date, file, multi-select, cross-field, repeater, and identity validators with their option shapes and message keys.
Validators run on every relevant change to a field's value. Each validator either lives in validators.validation on the field or container, or is implicitly attached by a widget at runtime. Every validator that can fire needs a matching entry in validation.messages — no validator is ever silent.
// Object form — required when the validator takes options
{ "name": "isLessThanField", "options": { "fieldKey": "MAX_BUDGET" } }
// String form — only valid for validators that take no options
"required"Validators that take options must use the object form. Passing a plain string drops the configuration silently and the validator becomes a no-op.
This page groups validators by purpose:
- Standard validators —
required,minimumAge - Date constraint validators —
minDate,maxDate,dateInPast,dateInFuture,dateRange - File upload validators —
minimumUploadSize,maximumUploadSize,invalidfileformat - Multi-select validators —
minSelection,maxSelection,multiSelectConflictValues - Cross-field validators —
isLessThanField,isGreaterThanField,uniqueField,jsonKeyValueIsEqualTo,jsonKeyValueIsNotEqualTo - Repeater validators —
uniqueRepeaterValues,repeaterUniqueField - Date comparison validator —
compareDateFields - Identity validator messages —
invalidNumberFormat,invalidNidInput,invalidId,invalidTIN,invalidAppNo,invalidNumber,invalidGenericInput,invalidInput
Standard validators
required
Key: required
When to use: Every mandatory field. Pair the validator key with the runtime expression from Form Rules — the validator alone is not enough for fields that hide.
{
"key": "FULL_NAME",
"type": "input",
"props": {
"label": "Full Name",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Full name is required."
}
}
}Common mistakes:
- Setting
props.required: truestatically — hidden fields still block submission. Use the three-property pattern instead. - Adding
"required"tovalidators.validationwithout settingvalidation.messages.required— no error text appears when the field is empty.
minimumAge
Key: minimumAge
When to use: Attach to a customidinput whose response payload includes a date of birth. The component auto-registers minimumAge (and maximumAge) when props.idConditions.ageLimit is set — do not add it to validators.validation by hand.
| Option | Type | Description |
|---|---|---|
props.idConditions.ageLimit.key | string | The field name in the NID response payload that holds the date of birth (e.g. "dateOfBirth"). |
props.idConditions.ageLimit.minimumAge | number | Minimum age in years. |
props.idConditions.ageLimit.maximumAge | number | Optional maximum age in years. |
{
"key": "APPLICANT_NID",
"type": "customidinput",
"props": {
"label": "National ID",
"required": false,
"defaultRequired": true,
"idType": "NID",
"endpointCode": "NIDAGETIDINFO",
"idConditions": {
"ageLimit": { "key": "dateOfBirth", "minimumAge": 18 }
},
"populates": [{ "valueKey": "dateOfBirth", "targetKey": "DATE_OF_BIRTH" }]
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "National ID is required.",
"minimumAge": "Applicant must be at least 18 years old."
}
}
}Common mistakes:
- Adding
"minimumAge"tovalidators.validationmanually — the component auto-registers it; duplicates cause double validation. - Setting
idConditions.ageLimit.keyto a form field key instead of the response payload key — the validator reads fromformState.FETCHED_DATA, so the key must match the API response property. - Using
minimumAgeon a standalonecustomdatepicker— the validator only fires for identity-fetch fields; it silently passes on date pickers.
Date constraint validators
The date constraints below are not Formly validators added to
validators.validation. They are props oncustomdatepickerthat restrict which dates the user can select. The error keysminDateandmaxDateappear invalidation.messagesto customise the message text when the user picks an out-of-range date.
Boundary string format
props.addRemoveTimeMinDate and props.addRemoveTimeMaxDate take a string "<amount>:<period>":
| Segment | Values | Notes |
|---|---|---|
amount | Any integer | Positive = future direction; negative = past direction; 0 = today. |
period | d (day), m (month), y (year), w (week) | Applied relative to today at form render time. |
Examples: "0:d" = today, "-1:d" = yesterday, "1:d" = tomorrow, "-18:y" = 18 years ago, "6:m" = six months from today.
dateInPast
Key: dateInPast (conceptual — uses maxDate message)
When to use: Restrict a date field so only past dates (and optionally today) are selectable.
{
"key": "ISSUE_DATE",
"type": "customdatepicker",
"props": {
"label": "Issue Date",
"required": false,
"defaultRequired": true,
"placeholder": "Select date",
"addRemoveTimeMaxDate": "0:d"
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Issue date is required.",
"maxDate": "Issue date cannot be in the future."
}
}
}Use "-1:d" for "strictly before today" (excludes today). Use "0:d" to include today.
dateInFuture
Key: dateInFuture (conceptual — uses minDate message)
When to use: Restrict a date field so only future dates (and optionally today) are selectable.
{
"key": "EXPIRY_DATE",
"type": "customdatepicker",
"props": {
"label": "Expiry Date",
"required": false,
"defaultRequired": true,
"placeholder": "Select date",
"addRemoveTimeMinDate": "0:d"
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Expiry date is required.",
"minDate": "Expiry date must be today or in the future."
}
}
}Use "1:d" for "strictly after today" (excludes today). Use "0:d" to include today.
maxDate
Key: maxDate
When to use: Restrict a date field to not exceed a specific relative upper bound.
{
"key": "APPLICATION_DATE",
"type": "customdatepicker",
"props": {
"label": "Application Date",
"required": false,
"defaultRequired": true,
"placeholder": "Select date",
"addRemoveTimeMaxDate": "90:d"
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Application date is required.",
"maxDate": "Application date must be within the next 90 days."
}
}
}Common mistakes:
- Confusing
props.maxDatewithprops.addRemoveTimeMaxDate—maxDatetakes anNgbDateStructobject and is for absolute fixed dates;addRemoveTimeMaxDatetakes a"number:period"string relative to today. Always useaddRemoveTimeMaxDatein form JSON.
minDate
Key: minDate
When to use: Restrict a date field to not be earlier than a specific relative lower bound.
{
"key": "VISA_VALID_FROM",
"type": "customdatepicker",
"props": {
"label": "Valid From",
"required": false,
"defaultRequired": true,
"placeholder": "Select date",
"addRemoveTimeMinDate": "-10:y"
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Valid from date is required.",
"minDate": "Valid from date cannot be more than 10 years in the past."
}
}
}Common mistakes:
- Confusing
props.minDatewithprops.addRemoveTimeMinDate— same distinction asmaxDateabove. Always useaddRemoveTimeMinDatein form JSON.
dateRange
Key: dateRange (conceptual — uses both minDate and maxDate messages)
When to use: Restrict a date field to a window between a minimum and maximum relative date.
{
"key": "TRAVEL_DATE",
"type": "customdatepicker",
"props": {
"label": "Travel Date",
"required": false,
"defaultRequired": true,
"placeholder": "Select date",
"addRemoveTimeMinDate": "0:d",
"addRemoveTimeMaxDate": "1:y"
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Travel date is required.",
"minDate": "Travel date cannot be in the past.",
"maxDate": "Travel date must be within one year from today."
}
}
}Common mistakes:
- Setting
addRemoveTimeMinDategreater thanaddRemoveTimeMaxDate— the calendar renders no selectable dates. - Using the same message text for
minDateandmaxDate— the user cannot tell which boundary they violated. Write distinct messages for each end.
File upload validators
The validators in this group are emitted by file-upload widgets (customfileupload, externalfileupload, custominternalfileupload) when the corresponding props.* constraint is violated. Configure the constraint as a prop, then provide the error text in validation.messages.
minimumUploadSize
Key: minimumUploadSize — fires when the uploaded file is smaller than props.minFileSize (bytes).
{
"key": "PASSPORT_SCAN",
"type": "customfileupload",
"props": {
"label": "Passport Scan",
"required": false,
"defaultRequired": true,
"minFileSize": 51200
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Passport scan is required.",
"minimumUploadSize": "File must be at least 50 KB."
}
}
}maximumUploadSize
Key: maximumUploadSize — fires when the uploaded file exceeds props.maxFileSize (bytes).
{
"key": "SUPPORTING_DOCUMENT",
"type": "customfileupload",
"props": {
"label": "Supporting Document",
"required": false,
"defaultRequired": true,
"maxFileSize": 5242880
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Supporting document is required.",
"maximumUploadSize": "File must be 5 MB or smaller."
}
}
}invalidfileformat
Key: invalidfileformat (lowercase is intentional — matches the enum entry exactly). Fires when the file's MIME type or extension is not in props.allowedFormats.
{
"key": "APPLICANT_PHOTO",
"type": "customfileupload",
"props": {
"label": "Applicant Photo",
"required": false,
"defaultRequired": true,
"allowedFormats": ["jpg", "jpeg", "png"],
"maxFileSize": 2097152
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Applicant photo is required.",
"invalidfileformat": "Only JPG and PNG files are allowed.",
"maximumUploadSize": "Photo must be 2 MB or smaller."
}
}
}Multi-select validators
minSelection
Key: minSelection — fires when the user picks fewer items than props.minSelection on multicheckbox or custommultiselectdatafetch.
{
"key": "LANGUAGES_SPOKEN",
"type": "multicheckbox",
"props": {
"label": "Languages Spoken",
"required": false,
"defaultRequired": true,
"minSelection": 1,
"options": [
{ "label": "Kinyarwanda", "value": "RW" },
{ "label": "English", "value": "EN" },
{ "label": "French", "value": "FR" }
]
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Select at least one language.",
"minSelection": "Select at least one language."
}
}
}maxSelection
Key: maxSelection — fires when the user picks more items than props.maxSelection.
{
"key": "TOP_PRIORITIES",
"type": "multicheckbox",
"props": {
"label": "Top Priorities",
"required": false,
"defaultRequired": true,
"minSelection": 1,
"maxSelection": 3,
"options": [
{ "label": "Cost", "value": "COST" },
{ "label": "Speed", "value": "SPEED" },
{ "label": "Quality", "value": "QUALITY" },
{ "label": "Support", "value": "SUPPORT" }
]
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Select at least one priority.",
"minSelection": "Select at least one priority.",
"maxSelection": "Select no more than 3 priorities."
}
}
}multiSelectConflictValues
Key: multiSelectConflictValues
When to use: Reject a multi-select submission when all of a configured list of conflicting values are selected at once.
| Option | Type | Description |
|---|---|---|
options | array | List of mutually-conflicting values; the validator fires only when ALL listed are selected. |
{
"key": "PERMISSIONS_REQUESTED",
"type": "multicheckbox",
"props": {
"label": "Permissions Requested",
"required": false,
"defaultRequired": true,
"options": [
{ "label": "Read", "value": "READ" },
{ "label": "Write", "value": "WRITE" },
{ "label": "Admin", "value": "ADMIN" }
]
},
"validators": {
"validation": [
"required",
{ "name": "multiSelectConflictValues", "options": ["WRITE", "ADMIN"] }
]
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Select at least one permission.",
"multiSelectConflictValues": "Cannot request both WRITE and ADMIN at once — choose one."
}
}
}Common mistakes:
- Passing it as a plain string instead of object form —
optionsis dropped and the conflict list is empty; the validator never fires. - Listing a single value in
options— the validator only triggers when every listed value is selected together. A one-element list is a no-op.
Cross-field validators
These compare a field's value against another top-level field's value. All cross-field validators must use the object form so their options are forwarded.
isLessThanField
Key: isLessThanField
When to use: This field's numeric value must be strictly less than another field's value.
| Option | Type | Description |
|---|---|---|
options.fieldKey | string | Key of the field to compare against. |
{
"key": "NUMBER_OF_OCCUPANTS",
"type": "input",
"props": {
"label": "Number of Occupants",
"type": "number",
"required": false,
"defaultRequired": true
},
"validators": {
"validation": [{ "name": "isLessThanField", "options": { "fieldKey": "NUMBER_OF_SEATS" } }]
},
"validation": {
"messages": {
"required": "Number of occupants is required.",
"isLessThanField": "Number of occupants cannot exceed the number of seats."
}
}
}isGreaterThanField
Key: isGreaterThanField
When to use: This field's numeric value must be strictly greater than another field's value.
| Option | Type | Description |
|---|---|---|
options.fieldKey | string | Key of the field to compare against. |
{
"key": "MAX_BUDGET",
"type": "input",
"props": {
"label": "Maximum Budget",
"type": "number",
"required": false,
"defaultRequired": true
},
"validators": {
"validation": [{ "name": "isGreaterThanField", "options": { "fieldKey": "MIN_BUDGET" } }]
},
"validation": {
"messages": {
"required": "Maximum budget is required.",
"isGreaterThanField": "Maximum budget must be greater than minimum budget."
}
}
}uniqueField
Key: uniqueField
When to use: Reject a value that duplicates the value of one or more other top-level fields. Common for "applicant email must differ from contact email" or "primary phone must differ from secondary phone".
| Option | Type | Description |
|---|---|---|
options.fieldKeys | object | Map keyed by the field that owns the validator. Each entry lists the other field keys to compare against and the message text. |
{
"key": "SECONDARY_PHONE",
"type": "customphonenumber",
"props": { "label": "Secondary Phone" },
"validators": {
"validation": [
{
"name": "uniqueField",
"options": {
"fieldKeys": {
"SECONDARY_PHONE": {
"fields": ["PRIMARY_PHONE"],
"message": "Secondary phone must differ from primary phone"
}
}
}
}
]
},
"validation": {
"messages": {
"uniqueField": "Secondary phone must differ from primary phone."
}
}
}Common mistakes:
- Pointing
fieldsat a key in a different repeater iteration —uniqueFieldcompares against top-level form values only. For cross-row uniqueness userepeaterUniqueFieldoruniqueRepeaterValues.
jsonKeyValueIsEqualTo
Key: jsonKeyValueIsEqualTo
When to use: When the field value is an object (e.g. a fetch response payload), enforce that one of its keys equals a known value. Common after a customgenericdatafetch call: "the returned status must be ACTIVE".
| Option | Type | Description |
|---|---|---|
options.key | string | Dot-path within the field value. |
options.value | any | Expected match value. |
{
"key": "POLICY_LOOKUP",
"type": "customgenericdatafetch",
"props": { "label": "Insurance Policy Number" },
"validators": {
"validation": [
{
"name": "jsonKeyValueIsEqualTo",
"options": { "key": "policyStatus", "value": "RENEWABLE" }
}
]
},
"validation": {
"messages": {
"jsonKeyValueIsEqualTo": "This policy cannot be renewed."
}
}
}jsonKeyValueIsNotEqualTo
Key: jsonKeyValueIsNotEqualTo
When to use: Inverse of jsonKeyValueIsEqualTo. Enforce that a key in the field's object value is not equal to a forbidden value.
| Option | Type | Description |
|---|---|---|
options.key | string | Dot-path within the field value. |
options.value | any | Forbidden value. |
{
"key": "APPLICATION_LOOKUP",
"type": "customgenericdatafetch",
"props": { "label": "Existing Application" },
"validators": {
"validation": [
{ "name": "jsonKeyValueIsNotEqualTo", "options": { "key": "state", "value": "REJECTED" } }
]
},
"validation": {
"messages": {
"jsonKeyValueIsNotEqualTo": "Cannot link to a rejected application."
}
}
}Repeater validators
uniqueRepeaterValues
Key: uniqueRepeaterValues
When to use: Attach to the repeater field itself (not a leaf field inside the row). Rejects the form when two rows duplicate the same value on a single property, or the same combination of values on multiple properties.
| Option | Type | Description |
|---|---|---|
options.propKeys | array | Each entry is either a string (single-property uniqueness) or an array of strings (combination uniqueness). |
{
"key": "BENEFICIARIES",
"type": "customrepeater",
"props": { "label": "Beneficiaries" },
"validators": {
"validation": [
{
"name": "uniqueRepeaterValues",
"options": {
"propKeys": ["NATIONAL_ID", ["FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH"]]
}
}
]
},
"validation": {
"messages": {
"uniqueRepeaterValues": "Each beneficiary must have a unique National ID, and the same person cannot be listed twice."
}
},
"fieldArray": { "fieldGroup": [] }
}Common mistakes:
- Attaching at a leaf field instead of the repeater —
control.valuewon't be an array and the validator never fires. - Forgetting to wrap multi-property keys in an inner array —
["firstName", "lastName"]at the top level enforces each property independently, not the combination.
repeaterUniqueField
Key: repeaterUniqueField
When to use: Per-row uniqueness inside a repeater. Attach to a leaf field inside the row template. Rejects the form if the same key value appears in another row.
No options — the validator uses the field's parent context to find the repeater and compare across siblings.
{
"key": "MEMBER_EMAIL",
"type": "input",
"props": { "label": "Member Email", "required": false, "defaultRequired": true, "type": "email" },
"validators": { "validation": ["email", "repeaterUniqueField"] },
"validation": {
"messages": {
"required": "Email is required.",
"repeaterUniqueField": "Each member must have a unique email."
}
}
}Common mistakes:
- Confusing
repeaterUniqueField(leaf field, no options) withuniqueRepeaterValues(repeater root, usespropKeys). - Adding either validator to a non-repeater field — both are no-ops outside repeater context.
Date comparison validator
compareDateFields
Key: compareDateFields
When to use: Enforce relative date constraints between pairs of date fields (e.g. issue date before expiry date). Place on the root sections container, not on individual date fields, so all date-pair comparisons are centralised in one fieldKeys object.
| Option | Type | Description |
|---|---|---|
options.fieldKeys | object | Map of source field keys to comparison rules. Put all date-pair rules here. |
options.fieldKeys[key][].comparator | string | "before", "after", "beforeOrEquals", "afterOrEquals", "equals". Fires when the assertion is FALSE. |
options.fieldKeys[key][].fieldKey | string | Target date field key. |
options.fieldKeys[key][].message | string | Error message when the constraint is violated. |
options.delimeter | string | Date string delimiter. Must match the date picker format. Default "/". |
{
"id": "SERVICE_FORM",
"type": "sections",
"validators": {
"validation": [
{
"name": "compareDateFields",
"options": {
"fieldKeys": {
"SHAREHOLDER_ISSUE_DATE": [
{
"comparator": "before",
"fieldKey": "SHAREHOLDER_EXPIRY_DATE",
"message": "The Expiry Date cannot be before the Issue Date"
}
],
"SHAREHOLDER_BIRTH_DATE": [
{
"comparator": "before",
"fieldKey": "SHAREHOLDER_ISSUE_DATE",
"message": "Passport issue date cannot be before date of birth"
}
]
}
}
}
]
},
"fieldGroup": []
}Common mistakes:
- Placing on an individual date field instead of the root
sectionscontainer — the validator searches the entire form model for field keys and must sit at the top level. - Adding as a plain string — it must use the object form with
options. - Confusing the direction —
"comparator": "before"on source A targeting B asserts "A is before B"; the error fires when A is NOT before B. - Scattering multiple
compareDateFieldsvalidators across individual fields — keep all date-pair rules in a singlefieldKeysobject on the root container.
Identity validator messages
The keys below are emitted by dedicated identity widgets (customidinput, customdoubleidinput, customcrvsidinput, customtininput, customapplicationnumberinput, customphonenumber) when their internal verification fails. With the exception of invalidNumberFormat (auto-attached to customphonenumber), these are not added to validators.validation by hand — they only need an entry in validation.messages.
| Message key | Source widget | Fires when |
|---|---|---|
invalidNumberFormat | customphonenumber | Phone number is not a valid international format. |
invalidNidInput | customidinput (idType: NID) | Entered NID does not match the configured nidType format. |
invalidId | All identity widgets | Generic lookup failure — the API returned no match or rejected the document. |
invalidTIN | customtininput | TIN format check or RDB/RRA lookup failed. |
invalidAppNo | customapplicationnumberinput | Application number format or lookup failed. |
invalidNumber | Number-format widgets | A value that should be numeric is not (e.g. customcurrencyformatinput). |
invalidGenericInput | genericUtilValidator pipeline | Fallback for the generic util pipeline; one catch-all message for the whole field. |
invalidInput | Last-resort fallback | Non-specific validation failure on any widget that surfaces the generic input error. |
Always provide custom messages — the defaults are generic. Example for an NID field:
{
"key": "APPLICANT_NID",
"type": "customidinput",
"props": {
"label": "National ID",
"required": false,
"defaultRequired": true,
"idType": "NID",
"nidType": "NATIONAL_ID",
"endpointCode": "NIDAGETIDINFO"
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "National ID is required.",
"invalidNidInput": "Enter a valid 16-digit Rwandan National ID number.",
"invalidId": "The ID number could not be found. Please verify and try again."
}
}
}Common mistakes:
- Adding the identity keys (e.g.
"invalidNidInput") tovalidators.validation— the component sets them programmatically; adding to the array has no effect. - Confusing
invalidNidInput(format mismatch) withinvalidId(lookup failure) — provide both messages so the user gets accurate feedback. - Omitting
validation.messages.invalidNumberFormatoncustomphonenumberwhen the default English text is not localised — provide a localised override.