How to Access a Ruby Hash with both String and Symbol Keys

How to Access a Ruby Hash with both String and Symbol Keys

September 27, 2023

Sometimes, you receive a Hash key as a method parameter or via user input, and you want to make the Hash understand that key as-is, without worrying if the key is a string or a symbol. ActiveSupport's `HashWithIndifferentAccess` class lets you accomplish this in a convenient way.

If you try to access a regular symbolized Hash in Ruby with a string key, it returns nil value.

framework = { name: 'Ruby on Rails', language: 'Ruby' }

framework[:name] # "Ruby on Rails"
framework['language'] # nil

To design a better method API, you may want to allow the users (or method callers) to pass any key, without worrying whether it has to be a string or a symbol.

To access the Hash without worrying about the type of the key, use the ActiveSupport::HashWithIndifferentAccess class provided in ActiveSupport library. It implements a hash where keys :name and "name" are considered to be the same.

require "active_support/hash_with_indifferent_access"

framework = ActiveSupport::HashWithIndifferentAccess.new
framework[:name] = 'Ruby on Rails'

puts framework[:name] # Ruby on Rails
puts framework['name'] # Ruby on Rails

You can also pass an existing Hash to the constructor to get a new instance:

require "active_support/hash_with_indifferent_access"

framework = { name: 'Ruby on Rails', language: 'Ruby' }
result = ActiveSupport::HashWithIndifferentAccess.new(framework)

puts result['name'] # Ruby on Rails
puts result[:language] # Ruby

If that long class name looks intimidating, don't worry, you don't have to initialize this class yourself. Instead, use the with_indifferent_access extension method.

require "active_support/core_ext/hash/indifferent_access"

framework = { name: 'Ruby on Rails', language: 'Ruby' }
result = framework.with_indifferent_access

puts result[:name] # Ruby on Rails
puts result['language'] # Ruby

The with_indifferent_access method is added by Active Support by monkey-patching the Hash class. It returns a new instance of the ActiveSupport::HashWithIndifferentAccess, initializing it with the current hash.

# lib/active_support/core_ext/hash/indifferent_access.rb

class Hash
def with_indifferent_access
ActiveSupport::HashWithIndifferentAccess.new(self)
end
end

This class supports all the existing hash API, so you can call any method you'd call on a regular Ruby hash.

A good real-world example is the params hash in Rails controllers, which can be accessed with either a string or a symbol.

params = ActionController::Parameters.new(name: "Ruby")

params[:name] # Ruby
params['name'] # Ruby

Behind the scenes, it uses the with_indifferent_access method as follows:

module ActionController
class Parameters
def initialize(parameters = {}, logging_context = {})
# ...

@parameters = parameters.with_indifferent_access
end
end
end

What about the performance?

If you're worried about performance, check out this Reddit post for a nuanced discussion: Why wouldn't you want a hash with indifferent access?

However, as the user stillness_still mentions:

I got 99 problems and the lookup time difference between a symbol and a string ain't one.

Well said, sir, well said.

Sign up for my newsletter

Let's learn to become better developers.

Comments

No comments yet. Be the first to leave one.

Sign in to leave a comment.