Transforming json file using jq so data ends up under a common object

Viewed 75

I have some json data i get from an API that i need to transform using jq into another json format for later use with ansible as part of an inventory script.

What i have is something like:

{
  "results": [
    {
      "name": "hostname1",
      "key1": "somevalue1",
      "key2": "somevalue2",
      "key3": "somevalue3"
    },
    {
      "name": "hostname2",
      "key1": "somevalue12",
      "key2": "somevalue22",
      "key3": "somevalue32"
    },
    {
      "name": "hostname3",
      "key1": "somevalue13",
      "key2": "somevalue23",
      "key3": "somevalue33"
    }
  ]
}

and i need to transform this to look like this:

{
  "_meta": {
    "hostvars": {
      "hostname1": {
        "name": "hostname1",
        "key1": "somevalue1",
        "key2": "somevalue2",
        "key3": "somevalue3"
      },
      "hostname2": {
        "name": "hostname2",
        "key1": "somevalue12",
        "key2": "somevalue22",
        "key3": "somevalue32"
      },
      "hostname3": {
        "name": "hostname3",
        "key1": "somevalue13",
        "key2": "somevalue23",
        "key3": "somevalue33"
      }
    }
  }
}

Things i have not been able to figure out. If i use something like:

cat input.json | jq '{_meta: { hostvars: { (.results[].name): (.results[]) } }}'

Then _meta and hostvars is repeated for each object in the input and that is not at all what i want, i need a common "header" and then the data under there.

Ideally i would like to also exclude the "name" part in the output since it is already used and duplicated, but this is just a bonus.

Advice on how to do this? or is the filter in jq always run against one object at a time? I experimented a bit with --slurp but didn't get anywhere

4 Answers

The crucial part is: Take an array of objects and transform it into one (outer) object where the attribute names of that outer object are given by some attribute value of the inner objects. That's the job description for INDEX(filter).

So:

{ _meta : { hostvars: ( .results | INDEX(.name) ) } } 

Using .results[] twice will iterate over the same list twice, giving you the cartesian product of each host with each of the other objects ( 3 x 3 = 9 ). You need to reference it once!

You can do something like below. The key to the logic is forming an array of objects, firstly by making the key name as .name and the value as the whole sub-object inside.

Once you have the array, you can un-wrap into a comma-separated list of objects using add.

{ _meta : { hostvars: ( ( .results | map( { ( .name ) : . } ) | add ) ) } } 

Demo - jqplay

Yet another approach using from_entries could be:

jq '.results | map({key: .name, value: .}) | {_meta: {hostvars: from_entries}}'
{
  "_meta": {
    "hostvars": {
      "hostname1": {
        "name": "hostname1",
        "key1": "somevalue1",
        "key2": "somevalue2",
        "key3": "somevalue3"
      },
      "hostname2": {
        "name": "hostname2",
        "key1": "somevalue12",
        "key2": "somevalue22",
        "key3": "somevalue32"
      },
      "hostname3": {
        "name": "hostname3",
        "key1": "somevalue13",
        "key2": "somevalue23",
        "key3": "somevalue33"
      }
    }
  }
}

Demo

You'll probably want to take advantage of jq pipes.

cat input.json | jq '{ _meta : { hostvars : (.results | map({key : .name, value :  del(. | .name) }) | from_entries) }}'

Map these results as if they were entries to a new set of entries where the key is .name, also removing the .name from .value as you do so:

.results | map({key : .name, value :  del(. | .name) })

Then, form an object from the entries:

... | from_entries

NOTE: See Inian's answer for why the array syntax used by the OP does not work.

Related