How to elegantly symbolize_keys for a 'nested' hash
Consider the following code:
hash1 = {"one" => 1, "two" => 2, "three" => 3}
hash2 = hash1.reduce({}){ |h, (k,v)| h.merge(k => hash1) }
hash3 = hash2.reduce({}){ |h, (k,v)| h.merge(k => hash2) }
hash4 = hash3.reduce({}){ |h, (k,v)| h.merge(k => hash3) }
hash4 is a 'nested' hash i.e. a hash with string keys and similarly 'nested' hash values.
The 'symbolize_keys' method for Hash in Rails lets us easily convert the string keys to symbols. But I'm looking for an elegant way to convert all keys (primary keys plus keys of all hashes within hash4) to symbols.
The point is to save myself from my (imo) ugly solution:
class Hash
def symbolize_keys_and_hash_values
symbolize_keys.reduce({}) do |h, (k,v)|
new_val = v.is_a?(Hash) ? v.symbolize_keys_and_hash_values : v
h.merge({k => new_val})
end
end
end
hash4.symbolize_keys_and_hash_values #=> desired result
FYI: Setup is Rails 3.2.17 and Ruby 2.1.1
Update:
Answer is hash4.deep_symbolize_keys
for Rails <= 5.0
Answer is JSON.parse(JSON[hash4], symbolize_names: true)
for Rails > 5
Solution 1:
There are a few ways to do this
-
There's a
deep_symbolize_keys
method in Railshash.deep_symbolize_keys!
-
As mentioned by @chrisgeeq, there is a
deep_transform_keys
method that's available from Rails 4.hash.deep_transform_keys(&:to_sym)
There is also a bang
!
version to replace the existing object. -
There is another method called
with_indifferent_access
. This allows you to access a hash with either a string or a symbol like howparams
are in the controller. This method doesn't have a bang counterpart.hash = hash.with_indifferent_access
-
The last one is using
JSON.parse
. I personally don't like this because you're doing 2 transformations - hash to json then json to hash.JSON.parse(JSON[h], symbolize_names: true)
UPDATE:
16/01/19 - add more options and note deprecation of deep_symbolize_keys
19/04/12 - remove deprecated note. only the implementation used in the method is deprecated, not the method itself.
Solution 2:
You cannot use this method for params or any other instance of ActionController::Parameters
any more, because deep_symbolize_keys
method is deprecated in Rails 5.0+ due to security reasons and will be removed in Rails 5.1+ as ActionController::Parameters
no longer
inherits from Hash
So this approach by @Uri Agassi seems to be the universal one.
JSON.parse(JSON[h], symbolize_names: true)
However, Rails Hash object still does have it.
So options are:
-
if you don't use Rails or just don't care:
JSON.parse(JSON[h], symbolize_names: true)
-
with Rails and ActionController::Parameters:
params.to_unsafe_h.deep_symbolize_keys
-
with Rails and plain Hash
h.deep_symbolize_keys
Solution 3:
In rails you can create HashWithIndifferentAccess class. Create an instance of this class passing your hash to its constructor and then access it with keys that are symbols or strings (like params of Controller's Actions):
hash = {'a' => {'b' => [{c: 3}]}}
hash = hash.with_indifferent_access
# equal to:
# hash = ActiveSupport::HashWithIndifferentAccess.new(hash)
hash[:a][:b][0][:c]
=> 3
Solution 4:
I can suggest something like this:
class Object
def deep_symbolize_keys
self
end
end
class Hash
def deep_symbolize_keys
symbolize_keys.tap { |h| h.each { |k, v| h[k] = v.deep_symbolize_keys } }
end
end
{'a'=>1, 'b'=>{'c'=>{'d'=>'d'}, e:'f'}, 'g'=>1.0, 'h'=>nil}.deep_symbolize_keys
# => {:a=>1, :b=>{:c=>{:d=>"d"}, :e=>"f"}, :g=>1.0, :h=>nil}
You can also easily extend it to support Arrays
:
class Array
def deep_symbolize_keys
map(&:deep_symbolize_keys)
end
end
{'a'=>1, 'b'=>[{'c'=>{'d'=>'d'}}, {e:'f'}]}.deep_symbolize_keys
# => {:a=>1, :b=>[{:c=>{:d=>"d"}}, {:e=>"f"}]}
Solution 5:
Might I suggest:
JSON.parse(hash_value.to_json)