jsonjq

jq: easiest way to recursively remove objects based on object value condition


I would like to use jq to remove all dictionaries within a JSON "object" (I used that term generally to refer to either an Array or a Dictionary) that

a) contain a key named "delete_me", AND b) where the key "delete_me" meets some predetermined condition (null, non-zero, true, etc)

Basically, the logic I want to implement is: walk the input, and at each node, if that node is not an Array or an Object, then keep it and move on, otherwise, keep it but remove from it any children that are dictionaries for which either condition a) or b) fail.

Any suggestions?

Sample input:

{
  "a": { "foo": "bar" },
  "b": {
    "i": {
      "A": {
        "i": [
          {
            "foo": {},
            "bar": {
              "delete_if_this_is_null": false,
              "an_array": [],
              "another_array": [
                {
                    "delete_if_this_is_null": null,
                    "foo": "bar"
                }
              ],
              "etc": ""
            },
            "foo2": "s"
          },
          {
            "foo": {
              "an_array": [
                {
                  "delete_if_this_is_null": "ok",
                  "foo":"bar",
                  "another_object": { "a":1 }
                },
                {
                  "delete_if_this_is_null": null,
                  "foo2":"bar2",
                  "another_object": { "a":1 },
                  "name": null
                }
              ],
              "an_object": {
                "delete_if_this_is_null":null,
                "foo3":"bar3"
              }
            },
            "zero": 0,
            "b": "b"
          }
        ]
      }
    }
  }
}

should yield, if the "delete_me" key is delete_if_this_is_null and the predetermined condition is delete_if_this_is_null == null:

{
  "a": { "foo": "bar" },
  "b": {
    "i": {
      "A": {
        "i": [
          {
            "foo": {},
            "bar": {
              "delete_if_this_is_null": false,
              "an_array": [],
              "another_array": [],
              "etc": ""
            },
            "foo2": "s"
          },
          {
            "foo": {
              "an_array": [
                {
                  "delete_if_this_is_null": "ok",
                  "foo":"bar",
                  "another_object": { "a":1 }
                }
              ]
            },
            "zero": 0,
            "b": "b"
          }
        ]
      }
    }
  }
}

UPDATE: Here's the solution: Assume the input is in a file 'input.json':
jq 'def walk(f):
  . as $in
  | if type == "object" then
      reduce keys[] as $key
        ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
  elif type == "array" then map( walk(f) ) | f
  else f
  end;

def mapper(f):
  if type == "array" then map(f)
  elif type == "object" then
  . as $in
  | reduce keys[] as $key
      ({};
       [$in[$key] | f ] as $value
       | if $value | length == 0 then .
         else . + {($key): $value[0]} end)
  else .
  end;

walk( mapper(select((type == "object" and .delete_if_this_is_null == null) | not)) )' < input.json

Solution

  • Jeff's solution may zap too much. For example, using:

    def data: [1,2, {"hello": {"delete_me": true, "a":3 }, "there": 4} ]; ];
    

    Jeff's solution yields empty (i.e. nothing).

    The following may therefore be closer to what you're looking for:

    walk(if (type == "object" and .delete_me) then del(.) else . end )
    

    For data, this yields:

    [1,2,{"hello":null,"there":4}]
    

    Alternative Solution

    If a solution that eliminates the "hello":null in the above example is required, then a variant of jq's map_values/1 is needed. Here's one approach:

    def mapper(f):
      if type == "array" then map(f)
      elif type == "object" then
      . as $in
      | reduce keys[] as $key
          ({};
           [$in[$key] | f ] as $value
           | if $value | length == 0 then . 
             else . + {($key): $value[0]} end)
      else .
      end;
    
    data | walk( mapper(select((type == "object" and .delete_me) | not)) )
    

    The result is:

    [1,2,{"there":4}]