Convert Hash to OpenStruct recursively

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.


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

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.


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

Weihang Jian showed a slight improvement to this here https://stackoverflow.com/a/69311716/2981429

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

Also see https://stackoverflow.com/a/63264908/2981429 which shows how to handle arrays

note

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.

Tags:

Ruby