bash json jq – How to Add/Modify Key/Value in JSON Object

bashjqjson

I've asked a similar question in the past but I'm having a hard time adapting that solution to this problem.

I have the following json array:

$ jq <<<"$json"
[
  {
    "id": "node1"
  },
  {
    "id": "node2"
  },
  {
    "id": "node3"
  }
]

I want to add a key/value to each node or modify it if it already exists. I can sort of do that with:

$ jq '.[] | select(.id == "node2") += {status: "fail"}' <<<"$json"
{
  "id": "node1"
}
{
  "id": "node2",
  "status": "fail"
}
{
  "id": "node3"
}

But notice the outer array disappears so when I try to implement this solution into my script it fails.

#!/usr/bin/env bash

[[ $DEBUG == true ]] && set -x

nodes=(node1 node2 node3)
json='[{"id": "node1"},{"id": "node2"},{"id": "node3"}]'

for node in "${nodes[@]}"; do
    if [[ $node == node2 ]]; then
        status=fail
    else
        status=pass
    fi
    json=$(jq --arg status "$status" --arg node "$node" '.[] | select(.id == $node) += {status: $status}' <<<"$json")
done

Error:

$ ./script.sh
jq: error (at <stdin>:4): Cannot index string with string "id"
jq: error (at <stdin>:7): Cannot index string with string "id"
jq: error (at <stdin>:10): Cannot index string with string "id"

Is there a way I can modify these json objects while retaining the overall structure?

Best Answer

Once you have picked out one of the elements from .[] with select(), that's the only data you're left with and what will be outputted at the end.

Instead, use map() to apply a (possible) modification to each of the elements of the top-level array:

$ jq --arg id node2 --arg status fail 'map(select(.id == $id) += { status: $status } )' file
[
  {
    "id": "node1"
  },
  {
    "id": "node2",
    "status": "fail"
  },
  {
    "id": "node3"
  }
]

Or possibly neater, with fewer braces,

$ jq --arg id node2 --arg status fail 'map(select(.id == $id).status = $status)' file
[
  {
    "id": "node1"
  },
  {
    "id": "node2",
    "status": "fail"
  },
  {
    "id": "node3"
  }
]

Replicating your shell loop in jq to avoid multiple invocations of jq:

$ jq --arg failnode node2 'map(.status = if .id == $failnode then "fail" else "pass" end)' file
[
  {
    "id": "node1",
    "status": "pass"
  },
  {
    "id": "node2",
    "status": "fail"
  },
  {
    "id": "node3",
    "status": "pass"
  }
]

Or, failing multiple nodes with each failed node listed on the command line (note the unorthodox but necessary placing of --args at the end):

$ jq 'map(.status = if (.id|IN($ARGS.positional[])) then "fail" else "pass" end)' file --args node1 node2
[
  {
    "id": "node1",
    "status": "fail"
  },
  {
    "id": "node2",
    "status": "fail"
  },
  {
    "id": "node3",
    "status": "pass"
  }
]