Recursion and paths: .., paths, walk

jq · Expert · 12 min

Schemaless or deeply nested JSON, like Kubernetes manifests or Terraform state, needs tools that work at any depth.

.. (recurse) outputs the input and every value nested inside it, depth-first. It answers "find X anywhere": .. | .image? // empty lists every image field no matter how deep, and .. | numbers every number. (? and // empty skip non-objects and nulls.)

A path is an array of keys and indexes, like ["spec","containers",0,"image"].

  • paths outputs the path of every node; paths(type == "number") filters by a predicate on the value; leaf_paths is paths(scalars).
  • path(expr) gives the paths an expression would access: path(..|select(type=="string" and test("secret"))).
  • getpath(p), setpath(p; v) and delpaths([p...]) read and write by path array. With reduce they enable generic transformations.
  • to_entries works on one level; paths works on all of them.

walk(f) applies f to every node bottom-up, rebuilding the document: walk(if type == "object" then del(.managedFields) else . end) strips a key everywhere, and walk(if type == "string" then ascii_downcase else . end) lowercases all strings.

Combining them, you can flatten a document to dotted keys: [paths(scalars) as $p | {key: $p | map(tostring) | join("."), value: getpath($p)}] | from_entries. This is invaluable for diffing two large configs.

Example

cat > k.json <<'EOF'
{"spec":{"containers":[{"name":"app","image":"app:1.2"},{"name":"log","image":"fluent:2"}],
 "meta":{"managedFields":[1],"labels":{"tier":"Web"}}}}
EOF
jq -c '[.. | .image? // empty]' k.json                        # ["app:1.2","fluent:2"]
jq -c '[paths(type == "string")] | .[0]' k.json               # ["spec","containers",0,"name"]
jq -c 'getpath(["spec","meta","labels","tier"])' k.json       # "Web"
jq -c 'setpath(["spec","replicas"]; 3) | .spec.replicas' k.json   # 3
jq -c 'walk(if type == "object" then del(.managedFields) else . end) | .spec.meta' k.json
# {"labels":{"tier":"Web"}}
jq -c '[paths(scalars) as $p | {key: ($p | map(tostring) | join(".")), value: getpath($p)}]
       | from_entries | keys[:2]' k.json
# ["spec.containers.0.image","spec.containers.0.name"]

Remember

  • .. visits every nested value; combine with ? and // empty
  • paths, getpath, setpath, delpaths work with path arrays
  • walk(f) transforms every node bottom-up

Watch out for

  • Using .. | .field without ?, which errors as soon as it reaches an array or scalar.

Check yourself

  1. Which removes every `managedFields` key at any depth?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: jqlang.org/manual/

Recursion and paths: .., paths, walk · jq · Topolearn