JSONata: flattening recursion that tracks path of recursion

345 Views Asked by At

I have a nested, recursive JSON of arbitrary depth where a node can either be a parent of more nodes (i.e. it has a structure array containing nodes, which is the case for schema or fieldGroup nodes) or it can be a leaf (field node). I try to use JSONata to map it to a flattened structure. I have difficulty to track the recursion path - in this case I need to build a path of ids inside each recursively mapped field, but I can't.

Source JSON with three levels of nesting ("House no." is level 3):

{
  "schema": {
    "id": "S05000058",
    "label": "Request to breed mice",
    "structure": [
      {
        "contains": {
          "field": {
            "id": "F05001343",
            "label": "Accept terms and conditions"
          }
        }
      },
      {
        "contains": {
          "field": {
            "id": "F05001344",
            "label": "Agree to share registration with ministry of mice"
          }
        }
      },
      {
        "contains": {
          "fieldGroup": {
            "id": "G05000496",
            "label": "Requesting person",
            "structure": [
              {
                "contains": {
                  "field": {
                    "id": "F05000059",
                    "label": "Last name"
                  }
                }
              },
              {
                "contains": {
                  "field": {
                    "id": "F05000060",
                    "label": "First name"
                  }
                }
              },
              {
                "contains": {
                  "fieldGroup": {
                    "id": "G05000428",
                    "label": "Street address",
                    "structure": [
                      {
                        "contains": {
                          "field": {
                            "id": "F00000053",
                            "label": "Street"
                          }
                        }
                      },
                      {
                        "contains": {
                          "fieldGroup": {
                            "id": "G05000429",
                            "label": "House no",
                            "structure": [
                              {
                                "contains": {
                                  "field": {
                                    "id": "F00000016",
                                    "label": "Number"
                                  }
                                }
                              },
                              {
                                "contains": {
                                  "field": {
                                    "id": "F00000016",
                                    "label": "Appendix"
                                  }
                                }
                              }
                            ]
                          }
                        }
                      }
                    ]
                  }
                }
              },
              {
                "contains": {
                  "fieldGroup": {
                    "id": "G00000113",
                    "label": "Location",
                    "structure": [
                      {
                        "contains": {
                          "field": {
                            "id": "F00000054",
                            "label": "Postal code"
                          }
                        }
                      },
                      {
                        "contains": {
                          "field": {
                            "id": "F00000035",
                            "label": "City"
                          }
                        }
                      }
                    ]
                  }
                }
              },
              {
                "contains": {
                  "field": {
                    "id": "F05000523",
                    "label": "Country"
                  }
                }
              }
            ]
          }
        }
      }
    ]
  }
}

My JSONata mapping uses the descendants path operator ** to flatten fieldGroups recursively, starting from the second nesting level.

https://try.jsonata.org/lfQdZcAe-

My problem is that I do not know how to get access to the ids of the parent nodes during ** recursion, therefore my $mapFields() function is unable to build an idPath for the recursion (last $mapFields() invocation below):

(
    $mapFields := function($datenfelder, $prefix) {
        $datenfelder.{
            "fields": [
                {
                    "label": label,
                    "idPath": $prefix & '.' & id
                }
            ]                                
        }
    };
    { 
    "idPath": schema.id,
    "sections": $append(
        /* top-level fields go into general section */
        {
            "title": "Top level fields",
            "fieldGroups": [
                {
                    "title": "",
                    "rows": $mapFields(schema.structure.contains.field, schema.id)
                }
            ]
        },
        /* subsequent sections contain top-level field groups */
        schema.structure.contains.fieldGroup.{
            "title": label,
            "idPath": $$.schema.id & '.' & id,
            "fieldGroups": [
                $append(
                    {
                        "title": "",
                        "rows": $mapFields(structure.contains.field, $$.schema.id & '.' & id)
                    },            
                    /* create fieldGroups recursively */
                    structure.contains.**.fieldGroup.{
                        "title": label,

                        /* how to build group idPath from third level: */
                        "idPath": $$.schema.id & '.' & id,
                        
                        /* how to pass recursive path prefix here: */
                        "rows": $mapFields($.structure.contains.field, ???) 
                    }
                )
            ]                 
        }
    )
})

My JSONata fails to produce complete idPaths starting from the fieldGroup "Street address" (which is on the second level):

{
  "idPath": "S05000058",
  "sections": [
    {
      "title": "Top level fields",
      "fieldGroups": [
        {
          "title": "",
          "rows": [
            {
              "fields": [
                {
                  "label": "Accept terms and conditions",
                  "idPath": "S05000058.F05001343"
                }
              ]
            },
            {
              "fields": [
                {
                  "label": "Agree to share registration with ministry of mice",
                  "idPath": "S05000058.F05001344"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "title": "Requesting person",
      "idPath": "S05000058.G05000496",
      "fieldGroups": [
        {
          "title": "",
          "rows": [
            {
              "fields": [
                {
                  "label": "Last name",
                  "idPath": "S05000058.G05000496.F05000059"
                }
              ]
            },
            {
              "fields": [
                {
                  "label": "First name",
                  "idPath": "S05000058.G05000496.F05000060"
                }
              ]
            },
            {
              "fields": [
                {
                  "label": "Country",
                  "idPath": "S05000058.G05000496.F05000523"
                }
              ]
            }
          ]
        },
        {
          "title": "Street address",
          "idPath": "S05000058.G05000428",
          "rows": {
            "fields": [
              {
                "label": "Street",
                "idPath": ".F00000053" /* incomplete */
              }
            ]
          }
        },
        {
          "title": "House no",
          
          /* incomplete, should be "S05000058.G05000428.G05000429": */
          "idPath": "S05000058.G05000429", 
          "rows": [
            {
              "fields": [
                {
                  "label": "Number",
                  "idPath": ".F00000016" /* incomplete */
                }
              ]
            },
            {
              "fields": [
                {
                  "label": "Appendix",
                  "idPath": ".F00000016" /* incomplete */
                }
              ]
            }
          ]
        },
        {
          "title": "Location",
          "idPath": "S05000058.G00000113",
          "rows": [
            {
              "fields": [
                {
                  "label": "Postal code",
                  "idPath": ".F00000054" /* incomplete */
                }
              ]
            },
            {
              "fields": [
                {
                  "label": "City",
                  "idPath": ".F00000035" /* incomplete */
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Note that it is not sufficient to pass $$.schema.id & '.' & id to the recursive $mapFields() call, as shown by the "House No" fieldGroup on the third level - all fields starting from the third level would get a fixed prefix which does not reflect the actual depth of recursion.

I have also tried to create a recursion manually as in How to flatten nested object to single depth object with JSONata?, but I didn't manage to iterate over the structure array during recursion.

1

There are 1 best solutions below

2
On

I believe the "parent" path operator is what you want here: https://stedi.link/NR2Ax3Z

You can check how the parent operator works in the docs: https://docs.jsonata.org/path-operators#-parent