Cross-field Validators
Validators that compare a field against another field, enforce uniqueness across repeater rows, or validate JSON-keyed response values.
These validators compare values across fields or across repeater rows. All cross-field validators must use the object form of the validator entry so their options are forwarded — passing a plain string loses the configuration.
// Correct — object form preserves options
{ "name": "isLessThanField", "options": { "fieldKey": "MAX_BUDGET" } }
// Wrong — plain string drops options silently
"isLessThanField"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"
}
}
}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.
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.
| 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": {
"ISSUE_DATE": [
{
"comparator": "before",
"fieldKey": "EXPIRY_DATE",
"message": "The Expiry Date cannot be before the Issue Date"
}
]
}
}
}
]
},
"fieldGroup": []
}Common mistakes:
- Placing on an individual date field instead of the root
sectionscontainer. - 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.