68

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

SHS
  • 7,651
  • 3
  • 18
  • 28
  • 1
    you can look at http://api.rubyonrails.org/classes/Hash.html#method-i-deep_symbolize_keys – jvnill Jul 24 '14 at 08:24
  • @jvnill - write it as an answer... – Uri Agassi Jul 24 '14 at 08:45
  • 1
    @SyedHumzaShah, however, be aware that you having to "deep symbolize" betrays your lack of knowledge of what actually is in your hash. Avoid the method and use your knowledge of how your data structure is built. If it is so totally dynamic that you do need deep symbolize, you might consider using a tree object instead, with its traversal methods and other assets pertinent to trees. – Boris Stitnicky Jul 29 '14 at 07:09
  • @BorisStitnicky if your hash has not been generated by you, e.g. a config file you're loading in, then it's common to not know what's actually in the hash... – Guillaume Roderick May 02 '17 at 13:14

6 Answers6

133

There are a few ways to do this

  1. There's a deep_symbolize_keys method in Rails

    hash.deep_symbolize_keys!

  2. 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.

  3. There is another method called with_indifferent_access. This allows you to access a hash with either a string or a symbol like how params are in the controller. This method doesn't have a bang counterpart.

    hash = hash.with_indifferent_access

  4. 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.

jvnill
  • 29,479
  • 4
  • 83
  • 86
  • 1
    This method is deprecated in rails >= 5.1,transform_keys is used instead [see this](https://github.com/netguru/messenger-ruby/pull/29) – chrisgeeq Jan 11 '19 at 11:28
  • [deep_symbolize_keys](https://api.rubyonrails.org/classes/Hash.html#method-i-deep_symbolize_keys) method is not deprecated at all. @chrisgeeq's comment points to a commit that uses `transform_keys` instead of deprecated `symbolize_keys` method. Please edit this answer to correct this. – genonymous Apr 11 '19 at 16:18
28

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
    
Mikhail Chuprynski
  • 2,404
  • 2
  • 29
  • 42
  • Any information for its deprecation? There is no notice of deprecation on [its documentation](http://api.rubyonrails.org/v5.0.0.1/classes/Hash.html#method-i-deep_symbolize_keys), and I am using it without encountering any deprecation warnings. – Franklin Yu Sep 15 '16 at 17:54
  • @FranklinYu I have edited answer adding some details. – Mikhail Chuprynski Sep 16 '16 at 18:18
  • 4
    It's deprecated on `ActionController::Parameters` (as implied in [this issue](https://github.com/activeadmin-plugins/active_admin_import/issues/91)), but not on `Hash` as I said above. As your quote says, *`ActionController::Parameters` no longer inherits from hash*. This question is about `Hash`. – Franklin Yu Sep 16 '16 at 21:55
  • Rails is in 6.1 and it's still not deprecated... It's an excellent answer, but I can't upvote it because it is misleading in this small issue. – user2553863 Aug 05 '22 at 00:36
6

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
Alexander
  • 7,484
  • 4
  • 51
  • 65
3

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"}]}
Uri Agassi
  • 36,848
  • 14
  • 76
  • 93
  • Thanks for the response, Uri. Apparently, there's a built-in 'deep_symbolize_keys' method that ships with Rails. I had gone through the Ruby docs but, stupidly, not through the Rails API docs. jvnill (above) was able to point me in the right direction. – SHS Jul 24 '14 at 08:34
  • @SyedHumzaShah - LOL, I wasn't aware of that either. It does not support arrays though :P – Uri Agassi Jul 24 '14 at 08:43
2

Might I suggest:

JSON.parse(hash_value.to_json)
mirage
  • 632
  • 10
  • 21
1

You could use:

  • Hash#to_s to convert the hash to a string;
  • String#gsub with a regex to convert the keys from strings to representations of symbols; and then
  • Kernel#eval to convert the string back into a hash.

This is an easy solution to your problem, but, you should only consider using it if you can trust that eval is not going to produce something nasty. If you have control over the content of the hash being converted, that should not be a problem.

This approach could be used for other kinds of nested objects, such as ones containing both arrays and hashes.

Code

def symbolize_hash(h)
  eval(h.to_s.gsub(/\"(\w+)\"(?==>)/, ':\1'))
end

Examples

symbolize_hash(hash4)
  #=> {:one=>{:one=>  {:one=>  {:one=>1, :two=>2, :three=>3},
  #                    :two=>  {:one=>1, :two=>2, :three=>3},
  #                    :three=>{:one=>1, :two=>2, :three=>3}},
  #           :two=>  {:one=>  {:one=>1, :two=>2, :three=>3},
  #                    :two=>  {:one=>1, :two=>2, :three=>3},
  #                    :three=>{:one=>1, :two=>2, :three=>3}},
  #           :three=>{:one=>  {:one=>1, :two=>2, :three=>3},
  #                    :two=>  {:one=>1, :two=>2, :three=>3},
  #                    :three=>{:one=>1, :two=>2, :three=>3}}},
  #    :two=>{:one=>  {:one=>  {:one=>1, :two=>2, :three=>3},
  #    ...
  #    :three=>{:one=>{:one=>  {:one=>1, :two=>2, :three=>3},
  #    ...
  #                    :three=>{:one=>1, :two=>2, :three=>3}}}}

symbolize_hash({'a'=>1, 'b'=>[{'c'=>{'d'=>'d'}}, {e:'f'}]})
  #=> {:a=>1, :b=>[{:c=>{:d=>"d"}}, {:e=>"f"}]}

Explanation

(?==>) in the regex is a zero-width postive lookahead. ?= signifies positive lookahead; => is the string that must immediately follow the match to \"(\w+)\". \1 in ':\1' (or I could have written ":\\1") is a string beginning with a colon followed by a backreference to the content of capture group #1, the key matching \w+ (without the quotes).

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100