[THIS IS NOT WORKING. READ BELOW.]

There are many ways to do this.

Assuming elements of the array are supposed to be unique, which your use case strongly implies, you can just pass the resulting array after the addition through the unique filter.

$ cat foo.json | jq '.widgets[] | select(.name=="foo").properties |= (.+ ["cat"] | unique)'

There are a few problems here.

One is that the resulting output is partial as it is missing the container object.

Another one is that the edited array looses the commas separating the objects thus becoming illegal JSON.

The actual result from the above command is:

{
  "name": "foo",
  "properties": [
    "baz",
    "cat"
  ]
}
{
  "name": "bar"
}
Answer from user3899165 on Stack Overflow
🌐
GitHub
gist.github.com › joar › 776b7d176196592ed5d8
Add a field to an object with JQ · GitHub
$ echo '{"hello": "world"}' | jq --arg foo bar '. + {foo: ("not" + $foo)}' { "hello": "world", "foo": "notbar" } ... I have json which is an object containing a property that is an array, and I want to create another property with the same value as an existing property, without changing anything else.
Top answer
1 of 2
2

You can use with_entries() function on the parent element to convert the sub elements into a pair keyed by key and value and add the string to value array, if it doesn't have it already

jq '.contact_groups |= ( with_entries( if ( .value | index("/contact_group/109") | not ) then .value += [ "/contact_group/109" ] else . end ) )'

The with_entries(..) builtin is a shorthand for doing to_entries | map | from_entries. The index("/contact_group/109") | not part ensures to add the entry if the string is not already present in the .value array.

See demo on jqplay

2 of 2
1

A variation of Inian's answer, but without the with_entries() call, with parametrization of the string we're adding, and with some minor simplifications to the logic:

$ jq --arg str '/contact_group/109' '.contact_groups |= map_values(if index($str) then . else . + [$str] end)' file
{
  "contact_groups": {
    "1": [
      "/contact_group/78",
      "/contact_group/109"
    ],
    "2": [
      "/contact_group/79",
      "/contact_group/109"
    ],
    "3": [
      "/contact_group/109"
    ],
    "4": [
      "/contact_group/109"
    ],
    "5": [
      "/contact_group/109"
    ]
  }
}

Another way to do this without the if statement is to use select() to only add stuff to the ones which index() returns null for:

jq --arg str '/contact_group/109' '.contact_groups |= map_values(select(index($str) == null) += [$str])' file

Assuming that the ordering of the bottom-level arrays is unimportant, we can be a bit more "sloppy" and add the string to all arrays while applying unique to remove duplicates:

$ jq --arg str '/contact_group/109' '.contact_groups |= map_values(. += [$str] | unique )' file
{
  "contact_groups": {
    "1": [
      "/contact_group/109",
      "/contact_group/78"
    ],
    "2": [
      "/contact_group/109",
      "/contact_group/79"
    ],
    "3": [
      "/contact_group/109"
    ],
    "4": [
      "/contact_group/109"
    ],
    "5": [
      "/contact_group/109"
    ]
  }
}

With a string that is already present in some arrays:

$ jq --arg str '/contact_group/79' '.contact_groups |= map_values(. += [$str] | unique )' file
{
  "contact_groups": {
    "1": [
      "/contact_group/78",
      "/contact_group/79"
    ],
    "2": [
      "/contact_group/79"
    ],
    "3": [
      "/contact_group/79"
    ],
    "4": [
      "/contact_group/79"
    ],
    "5": [
      "/contact_group/79"
    ]
  }
}

This approach would be useful if you'd wanted to add many strings to the arrays:

$ jq '.contact_groups |= map_values(. += $ARGS.positional | unique )' --args '/contact_group/79' '/contact_group/89' '/contact_group/99' <file
{
  "contact_groups": {
    "1": [
      "/contact_group/78",
      "/contact_group/79",
      "/contact_group/89",
      "/contact_group/99"
    ],
    "2": [
      "/contact_group/79",
      "/contact_group/89",
      "/contact_group/99"
    ],
    "3": [
      "/contact_group/79",
      "/contact_group/89",
      "/contact_group/99"
    ],
    "4": [
      "/contact_group/79",
      "/contact_group/89",
      "/contact_group/99"
    ],
    "5": [
      "/contact_group/79",
      "/contact_group/89",
      "/contact_group/99"
    ]
  }
}
🌐
Exercism
exercism.org › tracks › jq › concepts › arrays
Arrays in jq on Exercism
Array indexing is zero-based. Retrieve an element from an array with a bracket expression: .[2] gets the third element.
🌐
Sal Ferrarello
salferrarello.com › home › add element to array if it does not already exist with jq
Add Element to Array if It Does Not Already Exist with jq - Sal Ferrarello
3 weeks ago - If we add a value that is already in the array, we end up having it twice. echo '{"values": ["a", "b"]}' | jq --arg newvalue 'a' '.values += [$newvalue]' ... echo '{"values": ["a", "b"]}' | jq --arg newvalue 'c' 'if .values | index($newvalue) then "exists" else "does not exist" end'
🌐
iO Flood
ioflood.com › blog › jq-array
Manipulating JSON Arrays with jq | Example Guide
November 15, 2023 - In this example, we’re using the echo command to create an empty array, and then we’re using jq to add an element to it. The '. += ["element"]' part of the command is where the magic happens. This is jq’s syntax for adding an element to ...
Find elsewhere
🌐
jq recipes
remysharp.com › drafts › jq-recipes
jq recipes
April 16, 2024 - Recursively find all the properties whose key is errors whether it exists or not. The .. unrolls the object, the ? checks for the value or returns null and the select(.) is like a filter on truthy values: ... A generic CSV to JSON in jq.
🌐
CopyProgramming
copyprogramming.com › howto › add-new-element-to-existing-json-array-with-jq
Arrays: Using jq to Append a New Element to an Existing JSON Array
May 31, 2023 - Using jq to Append a New Element to an Existing JSON Array, Add to Array jQuery, Duplicate: Using jq to Add/Modify an Element for Each Object in an Array, Adding/Prepending an Element to the Beginning of an Array using jq
🌐
jq
jqlang.org › manual
jq 1.8 Manual
Once you understand the "," operator, you can look at jq's array syntax in a different light: the expression [1,2,3] is not using a built-in syntax for comma-separated arrays, but is instead applying the [] operator (collect results) to the expression 1,2,3 (which produces three different results). If you have a filter X that produces four results, then the expression [X] will produce a single result, an array of four elements.
Top answer
1 of 4
24

jq has a flag for feeding actual JSON contents with its --argjson flag. What you need to do is, store the content of the first JSON file in a variable in jq's context and update it in the second JSON

jq --argjson groupInfo "$(<input.json)" '.[].groups += [$groupInfo]' orig.json

The part "$(<input.json)" is shell re-direction construct to output the contents of the file given and with the argument to --argjson it is stored in the variable groupInfo. Now you add it to the groups array in the actual filter part.

Putting it in another way, the above solution is equivalent of doing this

jq --argjson groupInfo '{"id": 9,"version": 0,"lastUpdTs": 1532371267968,"name": "Training" }' \
   '.[].groups += [$groupInfo]' orig.json
2 of 4
15

This is the exact case that the input function is for:

input and inputs [...] read from the same sources (e.g., stdin, files named on the command-line) as jq itself. These two builtins, and jq’s own reading actions, can be interleaved with each other.

That is, jq reads an object/value in from the file and executes the pipeline on it, and anywhere input appears the next input is read in and is used as the result of the function.

That means you can do:

jq '.[].groups += [input]' orig.json input.json

with exactly the command you've written already, plus input as the value. The input expression will evaluate to the (first) object read from the next file in the argument list, in this case the entire contents of input.json.

If you have multiple items to insert you can use inputs instead with the same meaning. It will apply across a single or multiple files from the command line equally, and [inputs] represents all the file bodies as an array.

It's also possible to interleave things to process multiple orig files, each with one companion file inserted, but separating the outputs would be a hassle.

Top answer
1 of 2
2

"The question"

In answer to The question: Yes. jq 1.5 has keys_unsorted, so you can use the following def of walk/1, which is now standard in the “master” version of jq:

Copy# Apply f to composite entities recursively, and to atoms
def walk(f):
  . as $in
  | if type == "object" then
      reduce keys_unsorted[] as $key
        ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
  elif type == "array" then map( walk(f) ) | f
  else f
  end;

For further details and examples, see the “development” version of the jq manual, the jq FAQ https://github.com/stedolan/jq/wiki/FAQ, etc.

"No duplicates are allowed within the elements of an array"

This is readily accomplished using index/1; you might like to use a helper function such as:

Copydef ensure_has($x): if index([$x]) then . else . + [$x] end;

"If any of the parents of .name do not exist, should be able to add them on the fly"

If I understand this requirement correctly, it will useful for you to know that jq will create objects based on assignments, e.g.

{} | .a.b.c = 1

yields

Copy{"a":{"b":{"c":1}}}

Thus, using your example, you will probably want to include something like this in your walk:

Copyif type == "object" and has("spec")
   then (.spec.template.spec.containers? // null) as $existing
   | if $existing then .spec.template.spec.containers |= ... 
     else .spec.template.spec.containers = ...
     end
else .
end
2 of 2
0

Managed to reach a very good form:

  1. Added the following functions in ~/.jq:

    Copydef arr:
        if length<=0 then .[0] else .[] end;
    
    def arr(f):
        if length<=0 then
            .[0]
        else
            .[]|select(f)
        end//.[length];
    
    def when(COND; ACTION):
        if COND? // null then ACTION else . end;
    
    # Apply f to composite entities recursively, and to atoms
    def walk(f):
      . as $in
      | if type == "object" then
          reduce keys_unsorted[] as $key
            ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
      elif type == "array" then map( walk(f) ) | f
      else f
      end;
    
    def updobj(f):
      walk(when(type=="object"; f));
    
  2. A typical filter will look like this:

    Copyjq -r '{name:"CHANGEME",value: "xx"} as $v |
        map( when(.kind == "StatefulSet";
                  .spec.template.spec.containers|arr|.env|arr(.name==$v.name)) |= $v)'
    

The result will be that all objects that do not exist already will be created. The convention here is to use the arr functions for each object that you want to be an array, and at the end use a boolean condition and an object to replace the matched one or add to the parent array, if not matched.

  1. If you know the path is always there, and so are the object you want to update, walk is more elegant:

    Copyjq -r 'map(updobj(select(.name=="CHANGEME").value|="xx"))'
    

Thank you @peak for your effort and inspiring the solution.

🌐
GitHub
github.com › jqlang › jq › issues › 226
Adding unique item to array · Issue #226 · jqlang/jq
November 30, 2013 - I think this question indicates that I don't quite get how jq works. Given this data: $ DATA='{ "postgresServer":["d"], "playServer":["c"], "ignore":["b"], "generic":["a"] }' I want to add a new item to the ignore array: $ echo "$DATA" |...
Author   mslinn
Top answer
1 of 4
192

The |= .+ part in the filter adds a new element to the existing array. You can use jq with filter like:

jq '.data.messages[3] |= . + {
      "date": "2010-01-07T19:55:99.999Z", 
      "xml": "xml_samplesheet_2017_01_07_run_09.xml", 
      "status": "OKKK", 
      "message": "metadata loaded into iRODS successfullyyyyy"
}' inputJson

To avoid using the hardcoded length value 3 and dynamically add a new element, use . | length which returns the length, which can be used as the next array index, i.e.,

jq '.data.messages[.data.messages| length] |= . + {
      "date": "2010-01-07T19:55:99.999Z", 
      "xml": "xml_samplesheet_2017_01_07_run_09.xml", 
      "status": "OKKK", 
      "message": "metadata loaded into iRODS successfullyyyyy"
}' inputJson

(or) as per peak's suggestion in the comments, using the += operator alone

jq '.data.messages += [{
     "date": "2010-01-07T19:55:99.999Z",
     "xml": "xml_samplesheet_2017_01_07_run_09.xml", 
     "status": "OKKK", 
     "message": "metadata loaded into iRODS successfullyyyyy"
}]'

which produces the output you need:

{
  "report": "1.0",
  "data": {
    "date": "2010-01-07",
    "messages": [
      {
        "date": "2010-01-07T19:58:42.949Z",
        "xml": "xml_samplesheet_2017_01_07_run_09.xml",
        "status": "OK",
        "message": "metadata loaded into iRODS successfully"
      },
      {
        "date": "2010-01-07T20:22:46.949Z",
        "xml": "xml_samplesheet_2017_01_07_run_09.xml",
        "status": "NOK",
        "message": "metadata duplicated into iRODS"
      },
      {
        "date": "2010-01-07T22:11:55.949Z",
        "xml": "xml_samplesheet_2017_01_07_run_09.xml",
        "status": "NOK",
        "message": "metadata was not validated by XSD schema"
      },
      {
        "date": "2010-01-07T19:55:99.999Z",
        "xml": "xml_samplesheet_2017_01_07_run_09.xml",
        "status": "OKKK",
        "message": "metadata loaded into iRODS successfullyyyyy"
      }
    ]
  }
}

Use jq-play to dry-run your jq-filter and optimize any way you want.

2 of 4
79

Rather than using |=, consider using +=:

.data.messages += [{"date": "2010-01-07T19:55:99.999Z",
   "xml": "xml_samplesheet_2017_01_07_run_09.xml",
   "status": "OKKK", "message": "metadata loaded into iRODS successfullyyyyy"}]

Prepend

On the other hand, if (as @NicHuang asked) you want to add the JSON object to the beginning of the array, you could use the pattern:

 .data.messages |= [ _ ] + .
🌐
GitHub
gist.github.com › olih › f7437fb6962fb3ee9fe95bda8d2c8fa4
jq Cheet Sheet · GitHub
This works because adding an array to another array concatenates the elements: jq -cn '[1]+[2]' [1,2] jq -cn '["baz"]+["boo"]' ["baz","boo"] Copy link · Copy Markdown · Hoping I found the right place... I've been trying to figure out if there's a way to use jq in order to "flatten" nested json into simple dot-delimited "path.to.key=value" lines.