39

I'm trying to build an API wrapper gem, and having issues with converting hash keys to a more Rubyish format from the JSON the API returns.

The JSON contains multiple layers of nesting, both Hashes and Arrays. What I want to do is to recursively convert all keys to snake_case for easier use.

Here's what I've got so far:

def convert_hash_keys(value)
  return value if (not value.is_a?(Array) and not value.is_a?(Hash))
  result = value.inject({}) do |new, (key, value)|
    new[to_snake_case(key.to_s).to_sym] = convert_hash_keys(value)
    new
  end
  result
end

The above calls this method to convert strings to snake_case:

def to_snake_case(string)
  string.gsub(/::/, '/').
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
  gsub(/([a-z\d])([A-Z])/,'\1_\2').
  tr("-", "_").
  downcase
end

Ideally, the result would be similar to the following:

hash = {:HashKey => {:NestedHashKey => [{:Key => "value"}]}}

convert_hash_keys(hash)
# => {:hash_key => {:nested_hash_key => [{:key => "value"}]}}

I'm getting the recursion wrong, and every version of this sort of solution I've tried either doesn't convert symbols beyond the first level, or goes overboard and tries to convert the entire hash, including values.

Trying to solve all this in a helper class, rather than modifying the actual Hash and String functions, if possible.

Thank you in advance.

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
Andrew Stewart
  • 710
  • 1
  • 6
  • 10
  • Before doing anything else, `if (not ... and not ...)` is a perfect place to use the De Morgan's law. You should write it `unless ... or ...`. – sawa Jan 03 '12 at 01:39

7 Answers7

58

If you use Rails:

Example with hash: camelCase to snake_case:

hash = { camelCase: 'value1', changeMe: 'value2' }

hash.transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => "value2" }

source: http://apidock.com/rails/v4.0.2/Hash/transform_keys

For nested attributes use deep_transform_keys instead of transform_keys, example:

hash = { camelCase: 'value1', changeMe: { hereToo: { andMe: 'thanks' } } }

hash.deep_transform_keys { |key| key.to_s.underscore }
# => {"camel_case"=>"value1", "change_me"=>{"here_too"=>{"and_me"=>"thanks"}}}

source: http://apidock.com/rails/v4.2.7/Hash/deep_transform_keys

Hubert Olender
  • 1,130
  • 9
  • 15
  • this only transforms the first level of the hash, nested hash keys are still remained as camel case. – nayiaw Mar 10 '17 at 06:34
  • this is ok for high level but for nested hash also use deep_transform_keys. hash.deep_transform_keys { |key| key.to_s.underscore } – Shanaka Kuruwita Aug 18 '20 at 11:03
42

You need to treat Array and Hash separately. And, if you're in Rails, you can use underscore instead of your homebrew to_snake_case. First a little helper to reduce the noise:

def underscore_key(k)
  k.to_s.underscore.to_sym
  # Or, if you're not in Rails:
  # to_snake_case(k.to_s).to_sym
end

If your Hashes will have keys that aren't Symbols or Strings then you can modify underscore_key appropriately.

If you have an Array, then you just want to recursively apply convert_hash_keys to each element of the Array; if you have a Hash, you want to fix the keys with underscore_key and apply convert_hash_keys to each of the values; if you have something else then you want to pass it through untouched:

def convert_hash_keys(value)
  case value
    when Array
      value.map { |v| convert_hash_keys(v) }
      # or `value.map(&method(:convert_hash_keys))`
    when Hash
      Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
    else
      value
   end
end
mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • Works like a charm. Thank you kindly. I'm not doing this in Rails, but I believe the code I used for `to_snake_case` comes from the Rails `underscore` method. – Andrew Stewart Jan 03 '12 at 02:41
  • @Andrew: The `string.gsub(/::/, '/')` in `to_snake_case` makes me think you're right about where `to_snake_case` came from. Welcome to SO, enjoy your stay. – mu is too short Jan 03 '12 at 02:44
  • It isn't necessary to use Rails, just ActiveSupport, which lets us cherry-pick the routine. `require 'active_support/core_ext/string/inflections'` should do it: `'FooBar'.underscore => "foo_bar"`. – the Tin Man Jan 03 '12 at 02:47
  • 1
    The problem there is that I'm requiring the entire ActiveSupport gem when I only really need one method. – Andrew Stewart Jan 03 '12 at 02:59
14

I use this short form:

hash.transform_keys(&:underscore)

And, as @Shanaka Kuruwita pointed out, to deeply transform all the nested hashes:

hash.deep_transform_keys(&:underscore)
Alexey Grinko
  • 2,773
  • 22
  • 21
5

The accepted answer by 'mu is too short' has been converted into a gem, futurechimp's Plissken:

https://github.com/futurechimp/plissken/blob/master/lib/plissken/ext/hash/to_snake_keys.rb

This looks like it should work outside of Rails as the underscore functionality is included.

A Fader Darkly
  • 3,516
  • 1
  • 22
  • 28
2

Use deep_transform_keys for recursive conversion.

transform_keys only convert it in high level

hash = { camelCase: 'value1', changeMe: {nestedMe: 'value2'} }

hash.transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => {nestedMe: 'value2'} }

deep_transform_keys will go deeper and transform all nested hashes as well.

hash = { camelCase: 'value1', changeMe: {nestedMe: 'value2'} }

hash.deep_transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => {nested_me: 'value2'} }
Shanaka Kuruwita
  • 689
  • 2
  • 6
  • 19
0

If you're using the active_support library, you can use deep_transform_keys! like so:

hash.deep_transform_keys! do |key|
  k = key.to_s.snakecase rescue key
  k.to_sym rescue key
end
JayJay
  • 746
  • 1
  • 9
  • 21
0

This works both to camelCase and snake_case deep nested keys of an object, which is very useful for a JSON API:

def camelize_keys(object)
  deep_transform_keys_in_object!(object) { |key| key.to_s.camelize(:lower) }
end

def snakecase_keys(object)
  deep_transform_keys_in_object!(object) { |key| key.to_s.underscore.to_sym }
end

def deep_transform_keys_in_object!(object, &block)
  case object
  when Hash
    object.keys.each do |key|
      value = object.delete(key)
      object[yield(key)] = deep_transform_keys_in_object!(value, &block)
    end
    object
  when Array
    object.map! { |e| deep_transform_keys_in_object!(e, &block) }
  else
    object
  end
end
Pere Joan Martorell
  • 2,608
  • 30
  • 29