1

I am fetching droplet lists from the DigitalOcean JSON API and using jq map to transform the response into just the info I need.

I want to be able to add an optional key & value pair to my new object, based on whether the response contains a key with a certain value.

When using select() if the response doesn't contain a match, the whole droplet entry is removed from my new object, rather than just the key.

How can I filter a single key rather than the whole entry?

In detail:

The response for a single droplet looks like this:

 "id": 12345678,
 "name": "my-droplet",
 ...
 "networks": {
      "v4": [
        {
          "ip_address": "123.456.78.90",
          "netmask": "255.255.240.0",
          "gateway": "123.123.0.1",
          "type": "public"
        },
        {
          "ip_address": "10.123.45.67",
          "netmask": "255.255.0.0",
          "gateway": "10.123.0.1",
          "type": "private"
        }
      ],
      "v6": []
    },
 ...

I want to transform this into an object of the form:

{
  "id": 12345678,
  "name": "my-droplet",
  "public_ip": "123.456.78.90",
  "private_ip" "10.123.45.67"
} 

All droplets have a public IP, but the private IP is optional.

I have this, which ignores the optional private IP, and works fine so far:

jq '.droplets | map({id: .id, name: .name, status: .status, public_ip: .networks.v4[] | select(.type=="public") | .ip_address})'

However, once I add in the private IP, any droplet that doesn't have one goes missing from the response:

jq '.droplets | map({id: .id, name: .name, status: .status, public_ip: .networks.v4[] | select(.type=="public") | .ip_address, private_ip: .networks.v4[] | select(.type=="private") | .ip_address})'

I think I need to use concatenation or conditionals some how, but I can't quite figure out the syntax.

Note: I found this similar question, but it doesn't include changing the name of the key: denormalizing JSON with jq

ErisDS
  • 568
  • 4
  • 13
  • For clarity: if there is no private IP then ideally the key would not be present. I have no case where there are multiple private IPs. – ErisDS Feb 13 '18 at 09:46

1 Answers1

1

You don’t indicate precisely what should happen when there is no private ip (or indeed if there is more than one), but the following should get you on your way.

.droplets
| map({id, name, status, 
       public_ip: (first(.networks.v4[] | select(.type=="public"))  // {})
                  | .ip_address,
       private_ip: (first(.networks.v4[] | select(.type=="private")) // {})
                  | .ip_address } )

Conditional key

def one(condition): first(.[] | select(condition)) // null;

.droplets
| map({id, name, status}
      + (.networks.v4
         | { public_ip: one(.type=="public") | .ip_address}
           + (one(.type=="private") 
              | if . then {private_ip: .ip_address} else null end ) ))
peak
  • 105,803
  • 17
  • 152
  • 177
  • I added a comment to clarify that I was ideally looking to only add the key if it was present - but nulls work as well. Thanks! – ErisDS Feb 13 '18 at 09:50
  • Do you have any reference for the `// {}` syntax? I searched the jq manual at length looking for some sort of concept of defaults. – ErisDS Feb 13 '18 at 09:51
  • To avoid the null, you could either use `if then else end`, or delete the key if it is null. `//` is briefly documented in the manual, and more comprehensively in the jq FAQ https://github.com/stedolan/jq/wiki/FAQ – peak Feb 13 '18 at 09:55
  • I tested this solution out in jqplay, and it appears to expand out every possible combination of public vs private IP: https://jqplay.org/s/7ZvdfPwZ-G – ErisDS Feb 13 '18 at 10:19
  • Corrected parens, and added first() – peak Feb 13 '18 at 10:51
  • Works a treat – ErisDS Feb 13 '18 at 15:21
  • I wanted to come back and say thanks properly! This example has really helped me to understand how to compose jq programs, I've since been able to process a more complex JSON API example without any trouble. Thank you. – ErisDS Feb 21 '18 at 14:22