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.