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:
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.
dig method introduced in Ruby 2.3 can offer a slight improvement:
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.
fetch to the rescue
A built-in Hash
fetch offers another solution to the described issue. Let’s see it in action:
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
And the chaining looks kind of ugly. So let’s see how we can do it even better with a simple Hash extension.
So here’s our final implementation using the custom
deep_fetch Hash method:
deep_fetch works like a combination of
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:
ActionController::Parametersdoes not inherit from
Hash, so it has to be extended separately.
If you’re not fond of monkey-patching, you can use refinements instead:
HashHelpers in every class where you want to use the extension:
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.
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:
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.