'Convert Hash to OpenStruct recursively

Given I have this hash:

 h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }

And I convert to OpenStruct:

o = OpenStruct.new(h)
 => #<OpenStruct a="a", b="b", c={:d=>"d", :e=>"e"}> 
o.a
 => "a" 
o.b
 => "b" 
o.c
 => {:d=>"d", :e=>"e"} 
2.1.2 :006 > o.c.d
NoMethodError: undefined method `d' for {:d=>"d", :e=>"e"}:Hash

I want all the nested keys to be methods as well. So I can access d as such:

o.c.d
=> "d"

How can I achieve this?



Solution 1:[1]

personally I use the recursive-open-struct gem - it's then as simple as RecursiveOpenStruct.new(<nested_hash>)

But for the sake of recursion practice, I'll show you a fresh solution:

require 'ostruct'

def to_recursive_ostruct(hash)
  result = hash.each_with_object({}) do |(key, val), memo|
    memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
  end
  OpenStruct.new(result)
end

puts to_recursive_ostruct(a: { b: 1}).a.b
# => 1

edit

the reason this is better than the JSON-based solutions is because you can lose some data when you convert to JSON. For example if you convert a Time object to JSON and then parse it, it will be a string. There are many other examples of this:

class Foo; end
JSON.parse({obj: Foo.new}.to_json)["obj"]
# => "#<Foo:0x00007fc8720198b0>"

yeah ... not super useful. You've completely lost your reference to the actual instance.

Solution 2:[2]

You can monkey-patch the Hash class

class Hash
  def to_o
    JSON.parse to_json, object_class: OpenStruct
  end
end

then you can say

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
o = h.to_o
o.c.d # => 'd'

See Convert a complex nested hash to an object.

Solution 3:[3]

I came up with this solution:

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
json = h.to_json
=> "{\"a\":\"a\",\"b\":\"b\",\"c\":{\"d\":\"d\",\"e\":\"e\"}}" 
object = JSON.parse(json, object_class:OpenStruct)
object.c.d
 => "d" 

So for this to work, I had to do an extra step: convert it to json.

Solution 4:[4]

Here's a recursive solution that avoids converting the hash to json:

def to_o(obj)
  if obj.is_a?(Hash)
    return OpenStruct.new(obj.map{ |key, val| [ key, to_o(val) ] }.to_h)
  elsif obj.is_a?(Array)
    return obj.map{ |o| to_o(o) }
  else # Assumed to be a primitive value
    return obj
  end
end

Solution 5:[5]

My solution, based on max pleaner's answer and similar to Xavi's answer:

require 'ostruct'

def initialize_open_struct_deeply(value)
  case value
  when Hash
    OpenStruct.new(value.transform_values { |hash_value| send __method__, hash_value })
  when Array
    value.map { |element| send __method__, element }
  else
    value
  end
end

Solution 6:[6]

Here is one way to override the initializer so you can do OpenStruct.new({ a: "b", c: { d: "e", f: ["g", "h", "i"] }}).

Further, this class is included when you require 'json', so be sure to do this patch after the require.

class OpenStruct
  def initialize(hash = nil)
    @table = {}
    if hash
      hash.each_pair do |k, v|
        self[k] = v.is_a?(Hash) ? OpenStruct.new(v) : v
      end
    end
  end

  def keys
    @table.keys.map{|k| k.to_s}
  end
end

Solution 7:[7]

My solution is cleaner and faster than @max-pleaner's.

I don't actually know why but I don't instance extra Hash objects:

def dot_access(hash)
  hash.each_with_object(OpenStruct.new) do |(key, value), struct|
    struct[key] = value.is_a?(Hash) ? dot_access(value) : value
  end
end

Here is the benchmark for you reference:

require 'ostruct'

def dot_access(hash)
  hash.each_with_object(OpenStruct.new) do |(key, value), struct|
    struct[key] = value.is_a?(Hash) ? dot_access(value) : value
  end
end

def to_recursive_ostruct(hash)
  result = hash.each_with_object({}) do |(key, val), memo|
    memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
  end
  OpenStruct.new(result)
end

require 'benchmark/ips'
Benchmark.ips do |x|
  hash = { a: 1, b: 2, c: { d: 3 } }
  x.report('dot_access') { dot_access(hash) }
  x.report('to_recursive_ostruct') { to_recursive_ostruct(hash) }
end
Warming up --------------------------------------
          dot_access     4.843k i/100ms
to_recursive_ostruct     5.218k i/100ms
Calculating -------------------------------------
          dot_access     51.976k (± 5.0%) i/s -    261.522k in   5.044482s
to_recursive_ostruct     50.122k (± 4.6%) i/s -    250.464k in   5.008116s

Solution 8:[8]

Basing a conversion on OpenStruct works fine until it doesn't. For instance, none of the other answers here properly handle these simple hashes:

people = { person1: { display: { first: 'John' } } }
creds = { oauth: { trust: true }, basic: { trust: false } }

The method below works with those hashes, modifying the input hash rather than returning a new object.

def add_indifferent_access!(hash)
  hash.each_pair do |k, v|
    hash.instance_variable_set("@#{k}", v.tap { |v| send(__method__, v) if v.is_a?(Hash) } )
    hash.define_singleton_method(k, proc { hash.instance_variable_get("@#{k}") } )
  end
end

then

add_indifferent_access!(people)
people.person1.display.first # => 'John'

Or if your context calls for a more inline call structure:

creds.yield_self(&method(:add_indifferent_access!)).oauth.trust # => true

Alternatively, you could mix it in:

module HashExtension
  def very_indifferent_access!
    each_pair do |k, v|
      instance_variable_set("@#{k}", v.tap { |v| v.extend(HashExtension) && v.send(__method__) if v.is_a?(Hash) } )
      define_singleton_method(k, proc { self.instance_variable_get("@#{k}") } )
    end
  end
end

and apply to individual hashes:

favs = { song1: { title: 'John and Marsha', author: 'Stan Freberg' } }
favs.extend(HashExtension).very_indifferent_access!
favs.song1.title

Here is a variation for monkey-patching Hash, should you opt to do so:

class Hash
  def with_very_indifferent_access!
    each_pair do |k, v|
      instance_variable_set("@#{k}", v.tap { |v| v.send(__method__) if v.is_a?(Hash) } )
      define_singleton_method(k, proc { instance_variable_get("@#{k}") } )
    end
  end
end
# Note the omission of "v.extend(HashExtension)" vs. the mix-in variation.

Comments to other answers expressed a desire to retain class types. This solution accommodates that.

people = { person1: { created_at: Time.now } }
people.with_very_indifferent_access!
people.person1.created_at.class # => Time

Whatever solution you choose, I recommend testing with this hash:

people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 sschmeck
Solution 3 Daniel Viglione
Solution 4 Xavi
Solution 5 AlexWayfer
Solution 6 railsfanatic
Solution 7 Weihang Jian
Solution 8 Mark Schneider