Ruby Style: How to check whether a nested hash element exists

Todd R picture Todd R · Nov 30, 2009 · Viewed 49.6k times · Source

Consider a "person" stored in a hash. Two examples are:

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 

If the "person" doesn't have any children, the "children" element is not present. So, for Mr. Slate, we can check whether he has parents:

slate_has_children = !slate[:person][:children].nil?

So, what if we don't know that "slate" is a "person" hash? Consider:

dino = {:pet => {:name => "Dino"}}

We can't easily check for children any longer:

dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass

So, how would you check the structure of a hash, especially if it is nested deeply (even deeper than the examples provided here)? Maybe a better question is: What's the "Ruby way" to do this?

Answer

tadman picture tadman · Nov 30, 2009

The most obvious way to do this is to simply check each step of the way:

has_children = slate[:person] && slate[:person][:children]

Use of .nil? is really only required when you use false as a placeholder value, and in practice this is rare. Generally you can simply test it exists.

Update: If you're using Ruby 2.3 or later there's a built-in dig method that does what's described in this answer.

If not, you can also define your own Hash "dig" method which can simplify this substantially:

class Hash
  def dig(*path)
    path.inject(self) do |location, key|
      location.respond_to?(:keys) ? location[key] : nil
    end
  end
end

This method will check each step of the way and avoid tripping up on calls to nil. For shallow structures the utility is somewhat limited, but for deeply nested structures I find it's invaluable:

has_children = slate.dig(:person, :children)

You might also make this more robust, for example, testing if the :children entry is actually populated:

children = slate.dig(:person, :children)
has_children = children && !children.empty?