How to elegantly symbolize_keys for a 'nested' hash

后端 未结 6 2184
無奈伤痛
無奈伤痛 2021-02-06 20:46

Consider the following code:

  hash1 = {\"one\" => 1, \"two\" => 2, \"three\" => 3}
  hash2 = hash1.reduce({}){ |h, (k,v)| h.merge(k => hash1) }
  ha         


        
相关标签:
6条回答
  • 2021-02-06 21:05

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

    0 讨论(0)
  • 2021-02-06 21:12

    Might I suggest:

    JSON.parse(hash_value.to_json)
    
    0 讨论(0)
  • 2021-02-06 21:18

    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
    
    0 讨论(0)
  • 2021-02-06 21:19

    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
      
    0 讨论(0)
  • 2021-02-06 21:20

    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"}]}
    
    0 讨论(0)
  • 2021-02-06 21:24

    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.

    0 讨论(0)
提交回复
热议问题