Finding Deeply Nested Hash Keys
I often find myself wanting to find hash keys that are nested within a given hash. This is common when getting API responses, as they usually give back a ton of information. The way to handle these giant hashes usually involves manually traversing each level:
response_hash = # giant API response value_i_want = response_hash["key1"]["key2"]["key3"]
Or even worse, when the response hash contains arrays as values to hash keys:
response_hash = # giant API response value_i_want = response_hash["key1"]["key2"].first["key3"]
This requires you to know exactly how the hash is structured, which is a pain. What if you could just specify the key you wanted to find, and the work of traversing the hash could be done for you?
Here is a solution I came up with:
module DeepFind # obj is the hash we want to traverse, key is what we're looking for def deep_find(obj, key) # The base case for our recursive method. Returns if the key is found. return obj[key] if obj.respond_to?(:key?) && obj.key?(key) # If the object is either a Hash or Array if obj.is_a? Enumerable found = nil # Use the Enumerable#find to return the first object # for which the block returns true. Sets the found # variable to the result of calling #deep_find again. # With this recursive call, we pass in the last nested hash # of the current level of nesting as our object. This is # essentially a depth first search. obj.find { |*a| found = deep_find(a.last, key) } # return the value of the key found end end end
This is works for some cases, but we’re not quite done yet. What if the key you’re looking for is not unique? You could get a response hash that looks like this:
{ user: { telephone: { value: "180011111111" }, address: { value: "123 Boulevard" } } }
If you want to get the value
key for address
, it won’t work. The value of telephone will be found first, and the method will return.
To fix this, I added an optional nested_key
argument, so you can define the parent key first.
module DeepFind def deep_find(obj, key, nested_key: nil) return obj[key] if obj.respond_to?(:key?) && obj.key?(key) if obj.is_a? Enumerable found = nil obj.find { |*a| found = deep_find(a.last, key) } if nested_key.present? deep_find(found, nested_key) else found end end end end
Now you can find the value
key within address
by writing deep_find(response_hash, "address", nested_key: "value")
.
To use deep_find
in any Ruby class, just include the DeepFind
module:
class ApiInterface include DeepFind #... end