Andrew Stewart Andrew Stewart - 1 month ago 7
Ruby Question

Converting nested hash keys from CamelCase to snake_case in Ruby

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.

Answer

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