Last Updated: July 26, 2022
·
47.19K
· MeetDom

Convert a complex nested hash to an object

Sometimes in Ruby we need to convert a Hash into an object. Instead of accessing keys in Hash, we want to access attributes (methods) on an object. OpenStruct provides an easy way for us to do this: OpenStruct.new(hash).

But what do we do if we have a complex nested Hash with Arrays etc. OpenStruct doesn't recurse over Arrays and only works well with a flat structure. I've come across a number of Gems that attempt to solve this problem, and I've been using one in particular for the past year: recursive-open-struct. While it worked well in my particular scenario, others have expressed problems. Since I was refactoring the code that made use of this Gem, I thought I take a look at trying to remove the dependency.

I discovered a simpler way to achieve the same thing without the dependency on a Gem, and with a lot less code, and I thought I might share it with the community because I think it's pretty nifty.

We can get JSON to do the heavy lifting for us and instruct it to coerce nested attributes into OpenStructs. It will also remain respectful of Arrays.

Since both JSON and OpenStruct are in the Ruby Standard Library, we'll have no third-party dependencies.

First we need to require these two libraries:

require 'json'
require 'ostruct'

Let's create a complex nested Hash with Arrays:

hash = {:a=>1,:b=>[{:c=>2,:d=>[{:e=>3,:f=>4},{:e=>5,:f=>6}]},{:c=>4,:d=>[{:e=>7,:f=>8},{:e=>9,:f=>10}]},{:c=>6,:d=>[{:e=>11,:f=>12},{:e=>13,:f=>14}]}]}

Now let's convert the Hash to JSON:

json = hash.to_json
# => "{\"a\":1,\"b\":[{\"c\":2,\"d\":[{\"e\":3,\"f\":4},{\"e\":5,\"f\":6}]},{\"c\":4,\"d\":[{\"e\":7,\"f\":8},{\"e\":9,\"f\":10}]},{\"c\":6,\"d\":[{\"e\":11,\"f\":12},{\"e\":13,\"f\":14}]}]}"

Finally, let's parse the JSON instructing it to coerce objects with attributes into OpenStructs:

object = JSON.parse(json, object_class: OpenStruct)
# => #<OpenStruct a=1, b=[#<OpenStruct c=2, d=[#<OpenStruct e=3, f=4>, #<OpenStruct e=5, f=6>]>, #<OpenStruct c=4, d=[#<OpenStruct e=7, f=8>, #<OpenStruct e=9, f=10>]>, #<OpenStruct c=6, d=[#<OpenStruct e=11, f=12>, #<OpenStruct e=13, f=14>]>]>

Let's starting testing the object that's been created:

object.a
# => 1

As expected; now let's have a look at b:

object.b
# => [#<OpenStruct c=2, d=[#<OpenStruct e=3, f=4>, #<OpenStruct e=5, f=6>]>, #<OpenStruct c=4, d=[#<OpenStruct e=7, f=8>, #<OpenStruct e=9, f=10>]>, #<OpenStruct c=6, d=[#<OpenStruct e=11, f=12>, #<OpenStruct e=13, f=14>]>]

object.b.class
# => Array

object.b.size
# => 3

Since b is an array, we can access it's elements using an index:

object.b[0].class
# => OpenStruct

And each element is an object, so we can delve further to access it's attributes:

object.b[0].c
# => 2
object.b[0].d
# => [#<OpenStruct e=3, f=4>, #<OpenStruct e=5, f=6>]
object.b[0].d.class
# => Array
object.b[0].d.size
# => 2
object.b[0].d[1]
# => #<OpenStruct e=5, f=6>
object.b[0].d[1].f
# => 6

I love this little snippet of code. What was once achieved using a complex piece of code packaged into a Gem for reuse, is now 4 or 5 lines of code.

I hope you find this as useful as I did.

4 Responses
Add your response

wow this is great! Gotta love little hidden treats like this. Thank you so much!

over 1 year ago ·

So basically

# /config/initializers/hash.rb

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

Then you can call it anywhere in your project like

hash = { format: { html?: true }}

hash.to_o.format.html? # => true
over 1 year ago ·

This is wonderful! I will try that definitely with a large dataset I have in a deeply nested Json format

over 1 year ago ·

Really useful, thank you for sharing!

over 1 year ago ·