From 73b77481bee44a5cdbf5d8d0d6669d39213b75e8 Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 12 May 2026 12:13:38 +0200 Subject: [PATCH 1/4] docs: Add ADR for mapping and filtering in expressions --- ...6-05-11-maps-and-filters-in-expressions.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/adr/2026-05-11-maps-and-filters-in-expressions.md diff --git a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md new file mode 100644 index 00000000000..3df731398d4 --- /dev/null +++ b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md @@ -0,0 +1,270 @@ +# Support mapping and filtering in Altinn Studio expressions + +- Status: Accepted +- Deciders: Altinn Studio squad Data +- Date: 11.05.2026 + +## Result + +We have chosen alternative A6, using the JMESPath standard. We will first implement the variant with a separate, +source-independent function. Then we will need to discuss further on how to extend the `dataModel` and `component` +functions in a way that keeps the support for row context. + +## Problem context + +Altinn Studio supports a JSON-based expression language which runs in both backend and frontend. Currently, this +language only supports handling primitive values (strings, numbers and booleans). We have been requested to add list and +object support to make it possible to run functions with lists as input, such as sum and average. But those functions +are not sufficient by themselves. It must also be possible for the user to extract the list from a list of objects, +implying that we need to support mapping and filtering. + +## Decision drivers + +- **B1:** The solution should support projection (mapping). +- **B2:** It should support filtering. +- **B3:** It should theoretically (that is to say not considering memory limits) support selection of data from + composite objects and lists with an unlimited number of levels. +- **B4:** There should be no considerable risk for different outcomes in frontend and backend. +- **B5:** There must be a way to perform the extraction based on context data in repeating groups. +- **B6:** It should be convenient to implement and maintain. +- **B7:** It should not need to depend on lambda functions. (This might be considered 'nice to have' since there is a + very good chance we will need lambda functions at some point anyway. However, that introduces a whole new area of + decisions requiring careful consideration. Therefore, this point is crucial for finishing on time.) +- **B8:** Nice to have: It should be easily readable. +- **B9:** Nice to have: It should work on any list and object from any source, including hard-coded values. +- **B10:** Nice to have: It should not introduce new dependencies. +- **B11:** Nice to have: It should fit well together with the current `dataModel` selection syntax. +- **B12:** Nice to have: It should support "group by" operations. + +## Alternatives considered + +- **A1:** Function for mapping a list of objects to a single property. Example: + ```json + [ + "pluck", + ["list", { "name": "Ola", "city": "Oslo" }, { "name": "Kari", "city": "Hamar" }], + "name" + ] + ``` + returns + ```json + ["Ola", "Kari"] + ``` +- **A2:** Map and filter functions with lambda functions as input parameters. Example: + ```json + [ + "filter", + ["list", { "name": "Ola", "city": "Oslo" }, { "name": "Kari", "city": "Hamar" }], + ["Lambda function for filtering goes here. We still need to agree on some syntax for this."] + ] + ``` +- **A3:** String formatted mapping. + + Example extracting names from a list of people in the `dataModel` function: + + ```json + ["dataModel", "listOfPeople.name"] + ``` + + Example as separate function: + + ```json + [ + "query", + ["list", { "name": "Ola", "city": "Oslo" }, { "name": "Kari", "city": "Hamar" }], + "name" + ] + ``` + +- **A4:** String formatted mapping and filtering. + + Example extracting names of people aged 18 years or older in the `dataModel` function: + + ```json + ["dataModel", "listOfPeople[ageInYears>=18].name"] + ``` + + Example as separate function: + + ```json + [ + "query", + [ + "list", + { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, + { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ], + "[ageInYears>=18].name" + ] + ``` + +- **A5:** String formatted syntax using [the JSONPath standard](https://www.rfc-editor.org/rfc/rfc9535). + + Example in the `dataModel` function: + + ```json + ["dataModel", "$.listOfPeople[?@.ageInYears>=18].name"] + ``` + + Example as separate function: + + ```json + [ + "jsonpath", + [ + "list", + { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, + { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ], + "$[?@.ageInYears>=18].name" + ] + ``` + +- **A6:** String formatted syntax using [the JMESPath standard](https://jmespath.org/). + + Example in the `dataModel` function: + + ```json + ["dataModel", "listOfPeople[?ageInYears>=`18`].name"] + ``` + + Example as separate function: + + ```json + [ + "jmespath", + [ + "list", + { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, + { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ], + "[?ageInYears>=`18`].name" + ] + ``` + +- **A7:** String formatted syntax using [the JSONata standard](https://jsonata.org/). + + Example in the `dataModel` function: + + ```json + ["dataModel", "listOfPeople[ageInYears>=18].name"] + ``` + + Example as separate function: + + ```json + [ + "jsonata", + [ + "list", + { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, + { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ], + "$[ageInYears>=18].name" + ] + ``` + +## Pros and cons + +### A1 + +- Good: Adheres to B1, B4, B5, B6, B7, B8, B9, B10 and B11. +- Bad: It does not fulfill B2, B3 or B12. + +### A2 + +- Good: Adheres to B1, B2, B3, B4, B5, B6, B9, B10 and B11. +- Bad: It does not fulfill B7, meaning we cannot implement this before we have developed a specification for lambda + functions. +- Bad: It probably does not fulfill B8. + - No matter what syntax we choose for lambda functions, the expressions will quickly become very verbose, particularly + when nesting several map and filter functions together. +- Neutral: This alternative does not support B12 by itself, but having it in place, it should be fairly easy to add this + functionality as well. + +### A3 + +- Good: Adheres to B1, B3, B4, B5, B7, B8, B9, B10 and B11. +- Bad: It does not fulfill B2 (unless we implement filtering with lambda functions, which eliminates B7) or B12. +- Neutral: Fulfills B6 given that we have decided on the string syntax. + +### A4 + +- Good: Adheres to B1, B2, B3, B4, B5, B7, B8, B9, B10 and B11. +- Bad: Does not fulfill B6. + - We need to create a specification for the string syntax. + - We need to implement a new way to write comparison expressions that we already have. +- Bad: Does not fulfill B12. + +### A5 + +- Good: Adheres to B1, B2, B3, B6, B7, B8 and B9. +- Bad: Does not fulfill B4. + - [Lack of consensus on certain features makes libraries treat the queries differently.](https://cburgmer.github.io/json-path-comparison/) +- Neutral: Does not fulfill B5 in the sense that it is not possible to define custom functions, but we may solve this by + preprocessing. The use case may also be resolved by combining an isolated `jsonpath` function with the `component` + function. +- Bad: Does not fulfill B10. + - Introduces new dependencies and subdependencies in frontend and backend. +- Bad: Does not fulfill B11. + - Our `dataModel` selection syntax is similar, but there are slight differences (in particular when considering row + context). This means we need to find a convenient way to merge them together if we want to extend the `dataModel` + and `component` functions with this syntax. +- Bad: Does not fulfill B12. + +### A6 + +- Good: Adheres to B1, B2, B3, B6, B7, B8, B9 and B12. +- Good: Fulfills B4. + - It has had an unambiguous specification from the very beginning and a compliance test suite that libraries must + pass. +- Neutral: Does not fulfill B5 in the sense that it is not possible to define custom functions, but we may solve this by + preprocessing. The use case may also be resolved by combining an isolated `jmespath` function with the `component` + function. +- Bad: Does not fulfill B10. + - Good: [The Javascript library](https://www.npmjs.com/package/jmespath) has no subdependencies. + - Bad: [The Nuget package](https://www.nuget.org/packages/JmesPath.Net) depends on `Newtonsoft.Json`, which we are in + the process of replacing with + `System.Text.Json`. +- Bad: Does not fulfill B11. + - Our `dataModel` selection syntax is similar, but there are slight differences (in particular when considering row + context). This means we need to find a convenient way to merge them together if we want to extend the `dataModel` + and `component` functions with this syntax. + +### A7 + +- Good: Adheres to B1, B2, B3, B6, B7, B8, B9 and B12. +- Neutral: Does not fully support B4, but: + - [The Javascript library](https://www.npmjs.com/package/jsonata) is the official one + - [The maintainers of the C# library are planning to fix the few ways it currently differs from the Javascript library](https://github.com/mikhail-barg/jsonata.net.native#jsonata-language-features-support) +- Good: Fulfills B5. + - We may define a custom function for this. +- Bad: Does not fulfill B10. + - Good: [The Javascript library](https://www.npmjs.com/package/jsonata) has no subdependencies. + - Bad: [The Nuget package](https://www.nuget.org/packages/Jsonata.Net.Native) depends on `Newtonsoft.Json`, which we + are in the process of replacing with + `System.Text.Json`. +- Bad: Does not fulfill B11. + - Our `dataModel` selection syntax is similar, but there are slight differences (in particular when considering row + context). This means we need to find a convenient way to merge them together if we want to extend the `dataModel` + and `component` functions with this syntax. +- Bad: Not as widely established in the industry as JSONPath and JMESPath. +- Bad: Comes with an extensive set of functionality that we do not need or that our expression language already + supports. (But we might consider using this as a replacement for our entire expression language.) + +### Table + +| Decision driver | A1 | A2 | A3 | A4 | A5 | A6 | A7 | +| --------------- | --- | --- | --- | --- | --- | --- | --- | +| B1 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| B2 | ✗ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | +| B3 | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| B4 | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | — | +| B5 | ✓ | ✓ | ✓ | ✓ | — | — | ✓ | +| B6 | ✓ | ✓ | — | ✗ | ✓ | ✓ | ✓ | +| B7 | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | +| B8 | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | +| B9 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| B10 | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | +| B11 | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | +| B12 | ✗ | — | ✗ | ✗ | ✗ | ✓ | ✓ | From ee3963131a55f7d89188a091f500cf7be2727a3d Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 12 May 2026 12:28:39 +0200 Subject: [PATCH 2/4] Fixes based on Coderabbit's feedback --- docs/adr/2026-05-11-maps-and-filters-in-expressions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md index 3df731398d4..e632397408f 100644 --- a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md +++ b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md @@ -22,13 +22,13 @@ implying that we need to support mapping and filtering. - **B1:** The solution should support projection (mapping). - **B2:** It should support filtering. -- **B3:** It should theoretically (that is to say not considering memory limits) support selection of data from +- **B3:** It should theoretically (that is to say, not considering memory limits) support selection of data from composite objects and lists with an unlimited number of levels. - **B4:** There should be no considerable risk for different outcomes in frontend and backend. - **B5:** There must be a way to perform the extraction based on context data in repeating groups. - **B6:** It should be convenient to implement and maintain. - **B7:** It should not need to depend on lambda functions. (This might be considered 'nice to have' since there is a - very good chance we will need lambda functions at some point anyway. However, that introduces a whole new area of + strong likelihood we will need lambda functions at some point anyway. However, that introduces a whole new area of decisions requiring careful consideration. Therefore, this point is crucial for finishing on time.) - **B8:** Nice to have: It should be easily readable. - **B9:** Nice to have: It should work on any list and object from any source, including hard-coded values. From 3591b4f6b4f0bcdc9b6acffb0e4c0eafcb94239a Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 12 May 2026 12:54:45 +0200 Subject: [PATCH 3/4] Minor improvements --- docs/adr/2026-05-11-maps-and-filters-in-expressions.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md index e632397408f..1e5d987d051 100644 --- a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md +++ b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md @@ -58,7 +58,7 @@ implying that we need to support mapping and filtering. ["Lambda function for filtering goes here. We still need to agree on some syntax for this."] ] ``` -- **A3:** String formatted mapping. +- **A3:** String formatted mapping using a syntax we define by ourselves. Example extracting names from a list of people in the `dataModel` function: @@ -76,7 +76,7 @@ implying that we need to support mapping and filtering. ] ``` -- **A4:** String formatted mapping and filtering. +- **A4:** String formatted mapping and filtering using a syntax we define by ourselves. Example extracting names of people aged 18 years or older in the `dataModel` function: @@ -224,8 +224,7 @@ implying that we need to support mapping and filtering. - Bad: Does not fulfill B10. - Good: [The Javascript library](https://www.npmjs.com/package/jmespath) has no subdependencies. - Bad: [The Nuget package](https://www.nuget.org/packages/JmesPath.Net) depends on `Newtonsoft.Json`, which we are in - the process of replacing with - `System.Text.Json`. + the process of replacing with `System.Text.Json`. - Bad: Does not fulfill B11. - Our `dataModel` selection syntax is similar, but there are slight differences (in particular when considering row context). This means we need to find a convenient way to merge them together if we want to extend the `dataModel` @@ -242,8 +241,7 @@ implying that we need to support mapping and filtering. - Bad: Does not fulfill B10. - Good: [The Javascript library](https://www.npmjs.com/package/jsonata) has no subdependencies. - Bad: [The Nuget package](https://www.nuget.org/packages/Jsonata.Net.Native) depends on `Newtonsoft.Json`, which we - are in the process of replacing with - `System.Text.Json`. + are in the process of replacing with `System.Text.Json`. - Bad: Does not fulfill B11. - Our `dataModel` selection syntax is similar, but there are slight differences (in particular when considering row context). This means we need to find a convenient way to merge them together if we want to extend the `dataModel` From 6f73c71fb4e88c6e0f11b1ab3e202bd4335a3f64 Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 19 May 2026 13:34:19 +0200 Subject: [PATCH 4/4] Replace objects with object functions in examples --- ...6-05-11-maps-and-filters-in-expressions.md | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md index 1e5d987d051..eace9858a2d 100644 --- a/docs/adr/2026-05-11-maps-and-filters-in-expressions.md +++ b/docs/adr/2026-05-11-maps-and-filters-in-expressions.md @@ -42,7 +42,11 @@ implying that we need to support mapping and filtering. ```json [ "pluck", - ["list", { "name": "Ola", "city": "Oslo" }, { "name": "Kari", "city": "Hamar" }], + [ + "list", + ["object", "name", "Ola", "city", "Oslo"], + ["object", "name", "Kari", "city", "Hamar"] + ], "name" ] ``` @@ -54,7 +58,11 @@ implying that we need to support mapping and filtering. ```json [ "filter", - ["list", { "name": "Ola", "city": "Oslo" }, { "name": "Kari", "city": "Hamar" }], + [ + "list", + ["object", "name", "Ola", "city", "Oslo"], + ["object", "name", "Kari", "city", "Hamar"] + ], ["Lambda function for filtering goes here. We still need to agree on some syntax for this."] ] ``` @@ -71,7 +79,11 @@ implying that we need to support mapping and filtering. ```json [ "query", - ["list", { "name": "Ola", "city": "Oslo" }, { "name": "Kari", "city": "Hamar" }], + [ + "list", + ["object", "name", "Ola", "city", "Oslo"], + ["object", "name", "Kari", "city", "Hamar"] + ], "name" ] ``` @@ -91,8 +103,8 @@ implying that we need to support mapping and filtering. "query", [ "list", - { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, - { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ["object", "name", "Ola", "city", "Oslo", "ageInYears", 17], + ["object", "name", "Kari", "city", "Hamar", "ageInYears", 18] ], "[ageInYears>=18].name" ] @@ -113,8 +125,8 @@ implying that we need to support mapping and filtering. "jsonpath", [ "list", - { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, - { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ["object", "name", "Ola", "city", "Oslo", "ageInYears", 17], + ["object", "name", "Kari", "city", "Hamar", "ageInYears", 18] ], "$[?@.ageInYears>=18].name" ] @@ -135,8 +147,8 @@ implying that we need to support mapping and filtering. "jmespath", [ "list", - { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, - { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ["object", "name", "Ola", "city", "Oslo", "ageInYears", 17], + ["object", "name", "Kari", "city", "Hamar", "ageInYears", 18] ], "[?ageInYears>=`18`].name" ] @@ -157,8 +169,8 @@ implying that we need to support mapping and filtering. "jsonata", [ "list", - { "name": "Ola", "city": "Oslo", "ageInYears": 17 }, - { "name": "Kari", "city": "Hamar", "ageInYears": 18 } + ["object", "name", "Ola", "city", "Oslo", "ageInYears", 17], + ["object", "name", "Kari", "city", "Hamar", "ageInYears", 18] ], "$[ageInYears>=18].name" ]