7

I have an OpenStruct that is nested with many other OpenStructs. What's the best way to deeply convert them all to JSON?

Ideally:

x = OpenStruct.new
x.y = OpenStruct.new
x.y.z = OpenStruct.new
z = 'hello'

x.to_json
// {y: z: 'hello'}

Reality

{ <OpenStruct= ....> }
Breedly
  • 12,838
  • 13
  • 59
  • 83

6 Answers6

12

There is no default methods to accomplish such task because the built-in #to_hash returns the Hash representation but it doesn't deep converts the values.

If a value is an OpenStruct, it's returned as such and it's not converted into an Hash.

However, this is not that complicated to solve. You can create a method that traverses each key/value in an OpenStruct instance (e.g. using each_pair), recursively descends into the nested OpenStructs if the value is an OpenStruct and returns an Hash of just Ruby basic types.

Such Hash can then easily be serialized using either .to_json or JSON.dump(hash).

This is a very quick example, with an update from @Yuval Rimar for arrays of OpenStructs:

def openstruct_to_hash(object, hash = {})
  case object
  when OpenStruct then
    object.each_pair do |key, value|
    hash[key] = openstruct_to_hash(value)
    end
    hash
  when Array then
    object.map { |v| openstruct_to_hash(v) }
  else object
  end
end

openstruct_to_hash(OpenStruct.new(foo: 1, bar: OpenStruct.new(baz: 2)))
# => {:foo=>1, :bar=>{:baz=>2}}
TedMilker
  • 3,356
  • 1
  • 15
  • 7
Simone Carletti
  • 173,507
  • 49
  • 363
  • 364
  • Thanks for the detailed answer! I feel enlightened now! Seems `to_json` is pretty lame in this aspect since one would hope you could recursively convert to JSON. And why doesn't `OpenStruct` have a `to_json` method? – Breedly Oct 13 '15 at 00:22
  • `to_json` is a (bad?) habit made popular by programmers laziness. It's very simple to call `.to_json`, that simple that you often forget that having such global methods require each possible object to implement it or fall to the standard implementation. Whenever using complex objects, it's better to be explicit (either use JSON directly or for more complex objects such as ActiveRecord models use a serializer). – Simone Carletti Oct 13 '15 at 00:23
  • Honestly, I don't have a specific resource to point you to. It's the result of more than 10 years of Ruby development and maintenance of large applications. :) – Simone Carletti Oct 13 '15 at 00:25
9

Fixes to above solution to handle arrays

def open_struct_to_hash(object, hash = {})
  object.each_pair do |key, value|
    hash[key] = case value
                  when OpenStruct then open_struct_to_hash(value)
                  when Array then value.map { |v| open_struct_to_hash(v) }
                  else value
                end
  end
  hash
end
anka
  • 3,817
  • 1
  • 30
  • 36
Lance Gatlin
  • 193
  • 2
  • 9
4

Here's yet another approach, modified from lancegatlin's answer. Also adding the method to the OpenStruct class itself.

class OpenStruct
  def deep_to_h
    each_pair.map do |key, value|
      [
        key,
        case value
          when OpenStruct then value.deep_to_h
          when Array then value.map {|el| el === OpenStruct ? el.deep_to_h : el}
          else value
        end
      ]
    end.to_h
  end
lunarfyre
  • 1,636
  • 2
  • 14
  • 17
4

in initializers/open_struct.rb:

require 'ostruct'

# Because @table is a instance variable of OpenStruct and Object#as_json returns Hash of instance variables.
class OpenStruct
  def as_json(options = nil)
    @table.as_json(options)
  end
end

Usage:

OpenStruct.new({ a: { b: 123 } }).as_json

# Result
{
    "a" => {
        "b" => 123
    }
}

Edit:
This seems to do almost the same thing (notice the keys are symbols instead of strings)

OpenStruct.new({ a: { b: 123 } }).marshal_dump

# Result
{
    :a => {
        :b => 123
    }
}
Frexuz
  • 4,732
  • 2
  • 37
  • 54
1

Same function that can accept arrays as an input too

  def openstruct_to_hash(object, hash = {})
    case object
    when OpenStruct then
      object.each_pair do |key, value|
        hash[key] = openstruct_to_hash(value)
      end
      hash
    when Array then
      object.map { |v| openstruct_to_hash(v) }
    else object
    end
  end
Yuval Rimar
  • 1,055
  • 12
  • 22
0

None of the above worked for me with a copy and paste. Adding this solution:

require 'json'

class OpenStruct
  def deep_to_h
    each_pair.map do |key, value|
      [
        key,
        case value
          when OpenStruct then value.deep_to_h
          when Array then value.map {|el| el.class == OpenStruct ? el.deep_to_h : el}
          else value
        end
      ]
    end.to_h
  end
end

json=<<HERE

{
  "string": "fooval",
  "string_array": [
    "arrayval"
  ],
  "int": 2,
  "hash_array": [
    {
      "string": "barval",
      "string2": "bazval"
    },
    {
      "string": "barval2",
      "string2": "bazval2"
    }
  ]
}
HERE

os = JSON.parse(json, object_class: OpenStruct)
puts JSON.pretty_generate os.to_h
puts JSON.pretty_generate os.deep_to_h
kenberland
  • 61
  • 3