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

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: true statically — hidden fields still block submission. Use the three-property pattern instead.
  • Adding "required" to validators.validation without setting validation.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.

OptionTypeDescription
props.idConditions.ageLimit.keystringThe field name in the NID response payload that holds the date of birth (e.g. "dateOfBirth").
props.idConditions.ageLimit.minimumAgenumberMinimum age in years.
props.idConditions.ageLimit.maximumAgenumberOptional 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" to validators.validation manually — the component auto-registers it; duplicates cause double validation.
  • Setting idConditions.ageLimit.key to a form field key instead of the response payload key — the validator reads from formState.FETCHED_DATA, so the key must match the API response property.
  • Using minimumAge on a standalone customdatepicker — 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 on customdatepicker that restrict which dates the user can select. The error keys minDate and maxDate appear in validation.messages to 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>":

SegmentValuesNotes
amountAny integerPositive = future direction; negative = past direction; 0 = today.
periodd (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.maxDate with props.addRemoveTimeMaxDatemaxDate takes an NgbDateStruct object and is for absolute fixed dates; addRemoveTimeMaxDate takes a "number:period" string relative to today. Always use addRemoveTimeMaxDate in 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.minDate with props.addRemoveTimeMinDate — same distinction as maxDate above. Always use addRemoveTimeMinDate in 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 addRemoveTimeMinDate greater than addRemoveTimeMaxDate — the calendar renders no selectable dates.
  • Using the same message text for minDate and maxDate — 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.

OptionTypeDescription
optionsarrayList 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 — options is 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.

OptionTypeDescription
options.fieldKeystringKey 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.

OptionTypeDescription
options.fieldKeystringKey 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".

OptionTypeDescription
options.fieldKeysobjectMap 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 fields at a key in a different repeater iteration — uniqueField compares against top-level form values only. For cross-row uniqueness use repeaterUniqueField or uniqueRepeaterValues.

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

OptionTypeDescription
options.keystringDot-path within the field value.
options.valueanyExpected 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.

OptionTypeDescription
options.keystringDot-path within the field value.
options.valueanyForbidden 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.

OptionTypeDescription
options.propKeysarrayEach 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.value won'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) with uniqueRepeaterValues (repeater root, uses propKeys).
  • 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.

OptionTypeDescription
options.fieldKeysobjectMap of source field keys to comparison rules. Put all date-pair rules here.
options.fieldKeys[key][].comparatorstring"before", "after", "beforeOrEquals", "afterOrEquals", "equals". Fires when the assertion is FALSE.
options.fieldKeys[key][].fieldKeystringTarget date field key.
options.fieldKeys[key][].messagestringError message when the constraint is violated.
options.delimeterstringDate 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 sections container — 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 compareDateFields validators across individual fields — keep all date-pair rules in a single fieldKeys object 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 keySource widgetFires when
invalidNumberFormatcustomphonenumberPhone number is not a valid international format.
invalidNidInputcustomidinput (idType: NID)Entered NID does not match the configured nidType format.
invalidIdAll identity widgetsGeneric lookup failure — the API returned no match or rejected the document.
invalidTINcustomtininputTIN format check or RDB/RRA lookup failed.
invalidAppNocustomapplicationnumberinputApplication number format or lookup failed.
invalidNumberNumber-format widgetsA value that should be numeric is not (e.g. customcurrencyformatinput).
invalidGenericInputgenericUtilValidator pipelineFallback for the generic util pipeline; one catch-all message for the whole field.
invalidInputLast-resort fallbackNon-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") to validators.validation — the component sets them programmatically; adding to the array has no effect.
  • Confusing invalidNidInput (format mismatch) with invalidId (lookup failure) — provide both messages so the user gets accurate feedback.
  • Omitting validation.messages.invalidNumberFormat on customphonenumber when the default English text is not localised — provide a localised override.

On this page