-
Notifications
You must be signed in to change notification settings - Fork 13
ADR051: Add support for more comparisons to conditions #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| # ADRxxx: Add support for more comparisons to conditions | ||
|
|
||
| Date: 2026-06-11 | ||
|
|
||
| ## Status | ||
|
|
||
| Accepted | ||
|
|
||
| ## Context | ||
|
|
||
| > the facts behind the need to make the decision | ||
|
|
||
| We are currently adding support for multiple branches and multiple routing conditions for each question to GOV.UK Forms. As part of this we also want to allow multiple exit pages per selection question, and multiple selection options for a selection question to go to the same exit page. | ||
|
|
||
| While the current structure for form documents allows for multiple conditions per question, it only allows for a condition to be triggered by one possible selection option of a selection question: | ||
|
|
||
| ``` | ||
| # example of a form with a question with multiple conditions, in the current form document schema | ||
| # some properties omitted for brevity | ||
|
|
||
| { | ||
| "name": "A long form with branches", | ||
| "steps": [ | ||
| { | ||
| "id": "JJqRJCYs", | ||
| "data": { | ||
| "hint_text": null, | ||
| "answer_type": "selection", | ||
| "is_optional": false, | ||
| "page_heading": null, | ||
| "is_repeatable": false, | ||
| "question_text": "Where will you be studying?", | ||
| "answer_settings": { | ||
| "only_one_option": "true", | ||
| "selection_options": [ | ||
| { | ||
| "name": "England", | ||
| "value": "England" | ||
| }, | ||
| { | ||
| "name": "Wales", | ||
| "value": "Wales" | ||
| }, | ||
| { | ||
| "name": "Northern Ireland", | ||
| "value": "Northern Ireland" | ||
| }, | ||
| { | ||
| "name": "Scotland", | ||
| "value": "Scotland" | ||
| } | ||
| ] | ||
| }, | ||
| "guidance_markdown": null | ||
| }, | ||
| "type": "question", | ||
| "position": 3, | ||
| "next_step_id": "WHoH7UAZ", | ||
| "routing_conditions": [ | ||
| { | ||
| ..., | ||
| "skip_to_end": false, | ||
| "answer_value": "England", | ||
| "goto_page_id": "9GRmRKY4", | ||
| "check_page_id": "JJqRJCYs", | ||
| "routing_page_id": "JJqRJCYs", | ||
| "exit_page_heading": null, | ||
| "validation_errors": [], | ||
| "exit_page_markdown": null | ||
| }, | ||
| { | ||
| ..., | ||
| "skip_to_end": false, | ||
| "answer_value": "Wales", | ||
| "goto_page_id": "9GRmRKY4", | ||
| "check_page_id": "JJqRJCYs", | ||
| "routing_page_id": "JJqRJCYs", | ||
| "exit_page_heading": null, | ||
| "validation_errors": [], | ||
| "exit_page_markdown": null | ||
| }, | ||
| { | ||
| ..., | ||
| "skip_to_end": false, | ||
| "answer_value": "Northern Ireland", | ||
| "goto_page_id": "9GRmRKY4", | ||
| "check_page_id": "JJqRJCYs", | ||
| "routing_page_id": "JJqRJCYs", | ||
| "exit_page_heading": null, | ||
| "validation_errors": [], | ||
| "exit_page_markdown": null | ||
| } | ||
| ] | ||
| }, | ||
| ... | ||
| ], | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| This limitation is a particular problem for exit pages, where we want to avoid duplicating exit page content. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The proposed solution is really about enabling more complex routing logic (multi-value comparisons), rather than the problem it's motivated by: deduplicating exit page content. These are separate axes. The "in" operator dedupes conditions, but the content still lives on the condition. so the moment you want the same exit page from a different question, you're duplicating heading + markdown again. The core issue is that exit pages aren't normalised into a separate entity.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that having exit pages as a separate entity would be another way of solving this problem, and would in some ways would be preferable. That was one of the other options we considered. Unfortunately, it's a lot more work! I explored how to migrate exit page content from the conditions model to its own model safely, it would take at least 6 steps, without changing the form document schema (which we should probably do as well). We could do it with less steps with an hour's downtime, but I don't think it would be much quicker. It also doesn't help that at this point the design for exit pages has not yet been through usability testing, and the design for routing which motivates the design for exit pages is also only just now going through usability testing, so whatever we do could end up being unnecessary work if the result of the usability testing is that we need to make (further) large changes to the design. The proposed solution is an attempt to find a way to meet the constraints of the designs in a pragmatic way. As you say, it doesn't help with the case where multiple questions go to the same exit page, but as currently the design doesn't allow that, that is not an issue. That extra wrinkle from the design also complicates the case for splitting out the conditions I think; because it's not clear to me now where they should sit in the model or the form document schema. I also think the extra flexibility would be helpful generally. However, it's definitely a trade-off. It would be nice to have the time to split out conditions properly. We'd need to have a clear steer that the approach in this ADR is not what we want to do. |
||
|
|
||
| ## Decision | ||
|
|
||
| After exploring different options, we have decided to extend conditions so that one condition can be triggered by multiple different possible selection options for a selection question. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you talk more about the different options you explored?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a bunch of notes in this Google Doc: https://docs.google.com/document/d/1lm1vOcpevxeBJzBc8IDXXouSwBe2q6cjkNX-GWJGRGM/edit?pli=1&tab=t.yuz2c21e5ys4#heading=h.wjfjy9chxzgu I can add an outline of what was considered if that's helpful? Or would more detail be better? |
||
|
|
||
| We have decided to do this by expanding the form document schema for conditions so that `answer_value` can be a JSON object which describes a comparison: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overloading answer_value makes it polymorphic. Despite the backwards-compatible framing, it's effectively a breaking change, a consumer might get an object rather than an expected string. Have we considered introducing a separate attribute for these new comparisons? (Which would be additive, rather than breaking.)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that's a fair point, I hadn't considered the point about consumer expecting a string. I think though since our code doesn't have any expectation about the type of the data in I did consider adding a new attribute, but I thought that was itself also a breaking change, because currently So I was coming at it from the point of view of our existing code: it would do the wrong thing if it got a form document where there was an extra attribute for multiple answer values, whereas if the form document had something in |
||
|
|
||
| ``` | ||
| # example of a form with a question with a condition that triggers on multiple answer values, in the proposed form document schema | ||
| # some properties omitted for brevity | ||
|
|
||
| { | ||
| "name": "A long form with branches", | ||
| "steps": [ | ||
| { | ||
| "id": "JJqRJCYs", | ||
| "data": { | ||
| "hint_text": null, | ||
| "answer_type": "selection", | ||
| "is_optional": false, | ||
| "page_heading": null, | ||
| "is_repeatable": false, | ||
| "question_text": "Where will you be studying?", | ||
| "answer_settings": { | ||
| "only_one_option": "true", | ||
| "selection_options": [ | ||
| { | ||
| "name": "England", | ||
| "value": "England" | ||
| }, | ||
| { | ||
| "name": "Wales", | ||
| "value": "Wales" | ||
| }, | ||
| { | ||
| "name": "Northern Ireland", | ||
| "value": "Northern Ireland" | ||
| }, | ||
| { | ||
| "name": "Scotland", | ||
| "value": "Scotland" | ||
| } | ||
| ] | ||
| }, | ||
| "guidance_markdown": null | ||
| }, | ||
| "type": "question", | ||
| "position": 3, | ||
| "next_step_id": "WHoH7UAZ", | ||
| "routing_conditions": [ | ||
| { | ||
| ..., | ||
| "skip_to_end": false, | ||
| "goto_page_id": "9GRmRKY4", | ||
| "check_page_id": "JJqRJCYs", | ||
| "routing_page_id": "JJqRJCYs", | ||
| "exit_page_heading": null, | ||
| "exit_page_markdown": null, | ||
| "validation_errors": [], | ||
| "answer_value": { | ||
| "operator": "in", | ||
| "values": ["England", "Scotland", "Wales"], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What specifically do the "values" reference? Is the option selection "name" (e.g. the id) or the actual value? (I assume we want the "name" - as we want to be agnostic to the language being used (e.g. welsh or English)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the "value" of the selection option we want, the value is language independent, and is intended to be used for routing. See govuk-forms/forms-admin@c7ce55b, govuk-forms/forms-runner#1800. |
||
| } | ||
| } | ||
| ] | ||
| }, | ||
| ... | ||
| ], | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
|
|
||
| The structure chosen to describe the comparison looks generically like this: | ||
|
|
||
| ``` | ||
| { | ||
| "answer_value": { | ||
| "operator": ..., # string | ||
| ... # other propertie as required for the operator | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| This can in future support arbitraty operators, but for this ADR we define only one operator, "in", which checks whether the answer is included in the array `values`. | ||
|
|
||
| ``` | ||
| "answer_value": { | ||
| "operator": "in", | ||
| "values": [..., ...], # array of strings | ||
| } | ||
| ``` | ||
|
|
||
| As well as being forwards-compatible with future possible comparisons, this change is backwards-compatible with the existing form document schema. | ||
|
|
||
| Alongside `answer_value: nil`, and `answer_value: ... # String`, adding `answer_value: { operator: "in", values: ... }` gives us three different possible comparisons (any, is, and in). We could consider in a future ADR introducing a backwards-incompatible change to the ADR to rationalise all these comparisons to use the same structure, if we wished. | ||
|
|
||
| ## Consequences | ||
|
|
||
| Positive consequences: | ||
| - We can allow multiple selection options from one selection question to go to the same exit page | ||
| - We can reduce the size of form documents with selection questions that have multiple conditions routing from and going to the same question | ||
| - The form document schema changes are both backwards- and forwards-compatible | ||
|
|
||
| Negative consequences: | ||
| - The code for creating and updating conditions in forms-admin may be more complex | ||
| - There is ambiguity in the chosen schema, there are now multiple ways to represent the same condition | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We now have multiple ways of expressing the same logic i.e. answer_type: "example" is equivalent to answer_type: { operator: in, values: ["example"] } - which is confusing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that is messy, I agree. I'm not sure how big a problem it is though, what are your thoughts? |
||
|
|
||
| Additionally, as part of implementing this change, we will either change the column `answer_value` in the `conditions` table in the forms-admin database to be a `jsonb` type, or add a new `answer_value_json` column to that table, to hold the comparison JSON. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about when different questions have routing conditions to the "same" exit page?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current design precludes that; exit pages are associated with one and only one question, and can only be routed to from that question.