How can I repeat a value alongside nested values in a JMESPath query?

686 views Asked by At

Problem

I'd like to unpack an object into an array of objects, repeating a top-level key alongside values from a nested array. This seems trivial enough, but none of the tutorials or examples at jmespath.org cover this case.

Input data

{
  "name": "ryan",
  "pets": [
    "charlie",
    "michael",
    "snorlax",
    "socrates",
    "apollo"
  ]
}

Desired result

[
  {
    "owner": "ryan",
    "pet_name": "charlie"
  },
  {
    "owner": "ryan",
    "pet_name": "michael"
  },
  {
    "owner": "ryan",
    "pet_name": "snorlax"
  },
  {
    "owner": "ryan",
    "pet_name": "socrates"
  },
  {
    "owner": "ryan",
    "pet_name": "apollo"
  }
]

Attempted Solutions

This generates multiple objects, but I'm unclear on how to bring the owner key along for the ride:

$ jp -f test.json 'pets[].{owner: name, pet: @}'
[
  {
    "owner": null,
    "pet": "charlie"
  },
  {
    "owner": null,
    "pet": "michael"
  },
  {
    "owner": null,
    "pet": "snorlax"
  },
  {
    "owner": null,
    "pet": "socrates"
  },
  {
    "owner": null,
    "pet": "apollo"
  }
]

This brings in the right information, but doesn't generate multiple objects:

$ jp -f test.json '[{owner: name, pet_name: pets[] }]'
[
  {
    "owner": "ryan",
    "pet_name": [
      "charlie",
      "michael",
      "snorlax",
      "socrates",
      "apollo"
    ]
  }
]

2

There are 2 answers

0
michalskalski On

Please check similar question: JMESPath expression to flatten array of objects, each with nested arrays of objects it seems it is not possible to do in pure JMESPath expression as it operate on single scope. In the linked answer you will find examples how to handle this in context of ansible or using jq.

0
cr1stobal On

I was able to do accomplish this with a simple custom function. map_merge merges an object into an array of objects.

import json
import jmespath


class CustomFunctions(jmespath.functions.Functions):
    @jmespath.functions.signature({'types': ['object']}, {'types': ['array']})
    def _func_map_merge(self, obj, arg):
        result = []
        for element in arg:
            merged_object = super()._func_merge(obj, element)
            result.append(merged_object)
        return result


options = jmespath.Options(custom_functions=CustomFunctions())


source = """
{
  "name": "ryan",
  "pets": [
    "charlie",
    "michael",
    "snorlax",
    "socrates",
    "apollo"
  ]
}

"""

jmespath_expr = """
    map_merge({"owner": name}, pets[].{pet_name: @})
"""

result = jmespath.search(jmespath_expr, json.loads(source), options=options)
result

Given the code above you'll get your desired output like so:

[{'owner': 'ryan', 'pet_name': 'charlie'},
 {'owner': 'ryan', 'pet_name': 'michael'},
 {'owner': 'ryan', 'pet_name': 'snorlax'},
 {'owner': 'ryan', 'pet_name': 'socrates'},
 {'owner': 'ryan', 'pet_name': 'apollo'}]

I've been able to do some complex transforms using map_merge by "rolling context down" as jmespath traverses down the JSON tree.