Ruby Quick Tip - Use Deep Fetch for Nested Hash Values

 
Deep fetch hash helper method is represented by a manhole Photo by Plato Terentev from Pexels

Hashes are the most common data structures in Ruby and Rails apps. In this tutorial, I’ll describe a simple tip that makes working with hash values less prone to errors. It also improves code readability and provides a unified way of handling data structure issues.

That’s a lot of promises for a quick tip, so let’s get started!

How not to work with Ruby hashes…

Deeply nested hashes are first-class citizens in Rails apps, and it is a common practice to write code like that:

if params[:user][:comment][:body].present?
  # some logic
else
  # other logic
end

One disadvantage of this approach is that it implicitly assumes a hash structure. In this particular example, we’re working with params so an external source of data. By writing code like that, you’re allowing your users to crash the app because you’re optimistically assuming that the received data structure will always be correct. Invalid input could raise different errors depending on the payload.

A dig method introduced in Ruby 2.3 can offer a slight improvement:

begin
  if params.dig(:user, :comment, :body).present?
    # some logic
  else
    # other logic
  end
rescue TypeError
  # handle invalid structure
end

But dig API makes it impossible to differentiate the missing Hash key from the present key containing a nil value. And in practice, it’s often necessary to handle those two cases separately.

Hash fetch to the rescue

A built-in Hash fetch offers another solution to the described issue. Let’s see it in action:

begin
  if params.fetch(:user).fetch(:comment).fetch(:body).present?
    # some logic
  else
    # other logic
  end
rescue KeyError
  # handle missing key
end

For the price of a slightly more verbose implementation, we can now easily handle params with missing keys. But we’re still making an implicit assumption that received data will contain nested hashes in the accessed keys. Users could still crash our app by sending the following params:

{ user: { comment: nil } }

And the chaining looks kind of ugly. So let’s see how we can do it even better with a simple Hash extension.

Introducing deep_fetch

So here’s our final implementation using the custom deep_fetch Hash method:

begin
  if params.deep_fetch(:user, :comment, :body) { raise ParamsError, "Invalid input" }.present?
    # some logic
  else
    # other logic
  end
rescue ParamsError
  # handle invalid params
end

deep_fetch works like a combination of fetch and dig. Instead of returning nil when a key is not found, it raises a KeyError or returns a result of running a provided block. Here’s the monkey-patched implementation:

config/initializers/deep_fetch.rb

module DeepFetch
  def deep_fetch(*keys, &block)
    keys.reduce(self) do |hash_object, key|
      hash_object.fetch(key)
    end
  rescue NoMethodError, KeyError, ActionController::ParameterMissing => e
    if block_given?
      block.call
    else
      raise KeyError, e.message, e.backtrace
    end
  end
end

class Hash
  include DeepFetch
end

class ActionController::Parameters
  include DeepFetch
end
Surprisingly ActionController::Parameters does not inherit from Hash, so it has to be extended separately.


If you’re not fond of monkey-patching, you can use refinements instead:

module HashHelpers
  refine Hash do
    include DeepFetch
  end

  refine ActionController::Parameters do
    include DeepFetch
  end
end

and include HashHelpers in every class where you want to use the extension:

class UsersController
  using HashHelpers
end

By using deep_fetch, we can handle all the described cases. If the structure is invalid, we’ll receive an easy to rescue error, so users can no longer break our app by sending invalid input. Even if the received value is nil, we can be sure that it was extracted from correctly structured params.

Opinionated summary

deep_fetch could be a viable alternative to heavyweight libraries for validating the structure of any Ruby Hash. I’d even suggest going as far as assuming Hash bracket notation as an explicit sign that missing key is expected and should be handled accordingly. It means that the following code should not pass a code review:

value = params[:comment][:body]

It implicitly assumes that comment contains a Hash, so it is a highly probable source of random NoMethodError bugs on production. Adapting a deep fetching approach also makes sense for internal hash values, i.e. those not received from users. I’ve used to fight over this one in code reviews vs. comments like:

“What’s the point of using deep_fetch for the value that I AM SURE is there?”.

And that’s exactly the point! By using deep_fetch, you’re making it explicit that value must be there, and it’s clear that invalid structure is not expected.

I think that sticking to the convention of always deep fetching provides many benefits with minimal complexity overhead. Implementation is as simple as dropping in a dozen lines of code into your project, so I highly encourage you to give it a try.



Back to index