fix: evaluate value-dependent items conditionals per array row - #271
fix: evaluate value-dependent items conditionals per array row#271vermaxik wants to merge 3 commits into
Conversation
calculateFinalSchema pre-applies conditional rules by merging the matching
branch into the schema and deleting it. For array items it evaluated every
rule against an empty object, so the {}-matching branch was permanently
baked into the one shared items schema: rules with an else branch (or a
negated if) were wrong for every row, in both validation and field state.
Only constant conditionals (if: true / if: false) are pre-applied now --
their branch is row-independent, which is what the workaround originally
protected (schema-driven visibility inside items). Value-dependent rules
keep their then/else so validateSchema evaluates them per row against the
row's actual value.
Per-row FIELD mutations (visibility / required flags) remain unsupported
for group-array items: all rows share a single fields array.
eshiota
left a comment
There was a problem hiding this comment.
Everything looks solid! I left two nits as comments, and a request to add additional tests 😄
| return | ||
| } | ||
|
|
||
| const shouldProcessRule = (ifNode: JsfSchema | undefined): boolean => |
There was a problem hiding this comment.
Nit: this could be moved outside of the applySchemaRules's scope by injecting constantIfsOnly as an argument
| }) | ||
|
|
||
| it('evaluates negated conditionals per array item', () => { | ||
| // A negated `if` matches the empty object, so the old pre-processing baked |
There was a problem hiding this comment.
Nit: we don't need most of these comments, the code should be self-explanatory
There was a problem hiding this comment.
Could we add additional tests that cover some of the changes directly?
- An
if: trueclause inside anallOfat the level of a property withtype: 'object'
address: {
'type': 'object',
'x-jsf-presentation': { inputType: 'fieldset' },
'properties': { city: {...}, zip: {...} },
'allOf': [{
if: true,
then: { required: ['city'], properties: { zip: false } },
else: { required: ['zip'], properties: { city: false } },
}],
}
- An
if: trueclause directly at the level of a property withtype: 'object'
items: {
type: 'object',
properties: { a: {...}, b: {...} },
if: true,
then: { required: ['a'] },
else: { required: ['b'] },
}
- The same as above, but with a property with
itemsas an array
thanks for the review, addressed feedback in 590d4f7 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 8238cac. Configure here.
| * required flags) remain unsupported for group-array items. | ||
| */ | ||
| applySchemaRules(propertySchema.items as JsfObjectSchema, {}, options, jsonLogicContext) | ||
| applySchemaRules(propertySchema.items as JsfObjectSchema, {}, options, jsonLogicContext, true) |
There was a problem hiding this comment.
Flag dropped in branch processing
Low Severity
processBranch calls applySchemaRules without forwarding constantIfsOnly. A constant if on array items can still pre-apply nested value-dependent if/then/else against {} and delete those branches, so they never run per row.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8238cac. Configure here.


Summary
Conditionals (
if/then/elseinallOf) inside an array'sitemsschema areevaluated against an empty object instead of each row's value. Whichever branch
matches
{}gets permanently merged into the shareditemsschema and deleted,so rules with an
elsebranch (or a negatedif) validate wrongly for everyrow. This PR pre-applies only constant conditionals inside
itemsand leavesvalue-dependent ones intact, so
validateSchemaevaluates them per row.Json Schema
{ "title": "Per-row conditionals in nested arrays", "type": "object", "properties": { "discount_rules": { "type": "array", "title": "Discount rules", "x-jsf-presentation": { "inputType": "group-array", "addFieldText": "Add rule" }, "items": { "type": "object", "title": "Rule", "x-jsf-order": ["name", "mode", "percent", "tiering"], "required": ["name", "mode"], "x-jsf-presentation": { "inputType": "fieldset" }, "properties": { "name": { "type": "string", "title": "Name", "x-jsf-presentation": { "inputType": "text" } }, "mode": { "type": "string", "title": "Mode", "default": "flat", "oneOf": [ { "const": "flat", "title": "Flat" }, { "const": "tiered", "title": "Tiered" } ], "x-jsf-presentation": { "inputType": "radio" } }, "percent": { "type": ["string", "null"], "title": "Percent", "x-jsf-presentation": { "inputType": "text" } }, "tiering": { "type": ["object", "null"], "title": "Tiering", "x-jsf-order": ["period", "tiers"], "x-jsf-presentation": { "inputType": "fieldset" }, "properties": { "period": { "type": "string", "title": "Period", "oneOf": [ { "const": "monthly", "title": "Monthly" }, { "const": "yearly", "title": "Yearly" } ], "x-jsf-presentation": { "inputType": "select" } }, "tiers": { "type": "array", "title": "Tiers", "x-jsf-presentation": { "inputType": "group-array", "addFieldText": "Add tier" }, "items": { "type": "object", "title": "Tier", "x-jsf-order": ["up_to", "percent"], "required": ["percent"], "x-jsf-presentation": { "inputType": "fieldset" }, "properties": { "up_to": { "type": "integer", "title": "Up to", "x-jsf-presentation": { "inputType": "number" } }, "percent": { "type": "string", "title": "Percent", "x-jsf-presentation": { "inputType": "text" } } } } } } } }, "allOf": [ { "if": { "properties": { "mode": { "const": "tiered" } }, "required": ["mode"] }, "then": { "required": ["tiering"], "properties": { "tiering": { "type": "object", "required": ["period", "tiers"], "properties": { "tiers": { "minItems": 1, "x-jsf-errorMessage": { "minItems": "Add at least one tier, or switch the mode to flat." } } } }, "percent": { "maxLength": 0, "x-jsf-errorMessage": { "maxLength": "Not used for tiered rules. Clear it or switch the mode to flat." } } } }, "else": { "required": ["percent"], "properties": { "percent": { "type": "string" }, "tiering": { "properties": { "tiers": { "maxItems": 0, "x-jsf-errorMessage": { "maxItems": "Tiers only apply to tiered rules. Remove them or switch the mode." } } } } } } } ] } } } }Changes Made
calculateFinalSchemapre-applies conditional rules by merging the matchingbranch into the schema and deleting the branch. That is correct at the root,
where
valuesis the real form value, but foritemsthe existing workaroundin
applySchemaRulespassed a hardcoded{}:elsebranch had theelsebaked in for all rows (anifthat requires a field never matches
{}), producing wrong validation evenfor rows where the condition is true;
ifalways matches{}, baking thethenin for all rows;evaluation in
validateCondition(which is correct) never saw it.The change threads a
constantIfsOnlyflag throughapplySchemaRules. Insideitems, only conditionals whoseifis a boolean (if: true/if: false)are pre-applied — their branch is row-independent, which is what the original
workaround supported (schema-driven visibility inside items, covered by the
existing "with constant logic" tests). Value-dependent rules keep their
then/elseand are evaluated per item with the row's actual value.Known limitation, unchanged by this PR: per-row FIELD state (visibility,
required flags, titles) is still not representable, since all rows of a
group-array share a single fields array. This PR fixes validation only; the
describe.skip('with logic based on answers')for group-array field visibilitystays skipped.
Note
Medium Risk
Touches core conditional schema mutation in
applySchemaRules; behavior change is scoped to arrayitemsbut affects all forms with row-level JSON Schema conditionals.Overview
Fixes incorrect validation when array
itemsschemas use value-dependentif/then/elserules. Previously,applySchemaRulesran against{}for every shareditemsschema, permanently merging one branch and stripping the rule so all rows validated the same way—especially wrong forelsebranches and negatedifconditions.Now
applySchemaRulestakes aconstantIfsOnlyflag (viashouldProcessRule). For arrayitems, only booleanif(true/false) is pre-applied into the schema so row-independent visibility/required metadata still works. Value-dependent conditionals stay on the schema and are evaluated per row during validation.Adds array tests for
elsebranches, negated conditionals, and constantif: trueon nested objects and item schemas. Per-row field UI (visibility/required per group-array row) is unchanged and still out of scope.Reviewed by Cursor Bugbot for commit 8238cac. Bugbot is set up for automated code reviews on this repo. Configure here.