Use jq to alter nested objects in json files

March 26, 2021 (modified April 1, 2021)

Given a document structured like this, that is, containing an array of objects holding nested objects as properties

{
  "property-1": "value",
  "property-2": "value",
  "array": [
    {
      "stringprop": "value",
      "obj-1": {
        "sprop-1": "value",
        "sprop-2": "value"
      },
      "obj-2": {
        "sprop-1": "value",
        "sprop-2": "value"
      },
    },
    {
      "stringprop": "value",
      "obj-1": {
        "sprop-1": "value",
        "sprop-2": "value"
      },
      "obj-2": {
        "sprop-1": "value",
        "sprop-2": "value"
      },
    },
    ...
  ]
}

the task at hand is to add sprop-3 to all nested objects in array so that we end up with this

{
  "property-1": "value",
  "property-2": "value",
  "array": [
    {
      "stringprop": "value",
      "obj-1": {
        "sprop-1": "value",
        "sprop-2": "value",
        "sprop-3": "value"
      },
      "obj-2": {
        "sprop-1": "value",
        "sprop-2": "value",
        "sprop-3": "value"
      },
    },
    {
      "stringprop": "value",
      "obj-1": {
        "sprop-1": "value",
        "sprop-2": "value",
        "sprop-3": "value"
      },
      "obj-2": {
        "sprop-1": "value",
        "sprop-2": "value",
        "sprop-3": "value"
      },
    },
    ...
  ]
}

jq does this with the following filter set:

jq '.changeset = [ .changeset[] | .[] |= if (type == "string") then . else (. += {"extra": "stuff"}) end]'

Format that slightly differently to allow a line by line explanation

jq '.array =                               # replace array with
    [                                      # a new array that includes
      .array[]                             # every old element 
      | .[]                                #  for all properties of the element
        |=                                 #   replace the property with
          if (type == "string")            #      is it a string value?
          then .                           #    then itself
          else (. += {"sprop-3": "value"}) #      else the current value with the new propery added
          end
    ]'

This took me a little while to figure out. The pipe symbol introduces a new context, where the dot . stands for every result of the filter left of the pipe.