Zeeshan Zeeshan - 4 months ago 26
JSON Question

Custom to_json for nested complex objects in Ruby

I'm new to

Ruby
and having a little trouble
json
. I have inherited my classes with custom made
JSONable
class, as explained HERE in this answer. I have customized it according to my need, but I couldn't figure out how to make it work with custom nested (complex) objects, according to my requirement. I have following scenario.

First Class:

class Option < JSONable

def IncludeAll=(includeAll) #bool
@includeAll = includeAll
end

def IncludeAddress=(includeAddress) #bool
@includeAddress= includeAddress
end

......


Second Class:

class Search < JSONable

def CustomerId=(customerId)
@customerId = customerId
end

def identifier=(identifier)
@identifier = identifier
end

def Options=(options) #This is expected to be of Class Option, declared above
@options = options
end


Third Class:

class Request < JSONable

def DateTimeStamp=(dateTimeStamp)
@dateTimeStamp = dateTimeStamp
end

def SDKVersion=(sDKVersion)
@sDKVersion = sDKVersion
end

def RequestMessage=(requestMessage) #This is of type Search, declared above
@requestMessage = requestMessage
end


I call it as:

search = Search.new
searchOpts = Options.new
request = Request.new

search.identifier = identifier

searchOpts.IncludeAll = false
searchOpts.IncludeAddress = true

search.Options = searchOpts #setting nested level2 property here

//THE MOST OUTER CLASS OBJECT
request.SDKVersion = "xyz"
request.RequestMessage = search #setting nested level1


My ultimate goal is to send this
request
object to an
API
, after converting it to JSON. so i call
to_json
on
request
object as:

request.to_json


But here, suggested solution in that post (JSONable) fails in this case, as it can't convert the nested complex objects
request.search
and
request.search.Options
to Json.

(gives error: in 'to_json': wrong number of arguments (1 for 0) (ArgumentError)')

What I tried:

class JSONable
def to_json
hash = {}
self.instance_variables.each do |var|
#hash[var] = self.instance_variable_get var #tried to apply following check

if((self.instance_variable_get var).instance_of? Options ||((varVal).instance_of? Search))
varVal = self.instance_variable_get var
hash[var] = varVal.to_json #convert inner object to json
else
hash[var] = self.instance_variable_get var
end

end
hash.to_json
end
.....


This converts the nested model without any problem, but it messes up the 3rd level json. The result is as following:

{"DateTimeStamp":"121212","SDKVersion":"1.5","Culture":"en","RequestMessage":"{\"identifier\":\"851848913\",\"Options\":\"{\\\"IncludeAll\\\":true,\\\"IncludeAssociatedEntities\\\":true,\\\"IncludeAddress\\\":true,\\\"IncludePaymentInstructions\\\":true}\"}"}


And API doesn't respond. It seems as it messes up the boolean variables, which should be something like:

"SearchOption":"{\"IncludeAll\":true,\"IncludeAssociatedEntities\":true,\...


but it gives:

"SearchOption\":\"{\\\"IncludeAll\\\":true,\\\"IncludeAssociatedEntities\\\":true,\\\"Includ...


So the API logic can't cast it to corresponding
bool
objects anymore. JSON validator also fails to validate this result, i checked online

Questions:


  • How can I avoid this, and produce valid JSON in this case?

  • How can I apply generic check to in my JSONable class to check if the object is of some custom class / complex object.



(currently i have checked only for specific classes as:)

if((self.instance_variable_get var).instance_of? Options ||((varVal).instance_of? Search))


Other Info:


  • It works fine for all complex objects, having no nested objects

  • API is developed in .NET

  • I'm not using Rails, its a Ruby console app (I'm new to Ruby)


Answer

The answer you referred is dated “Dec 2010.” JSON library is included in ruby stdlib for years already and it perfectly converts Hash instances to json. That said, you just need to construct hashes out of your objects and then call JSON.dump on the resulting hash. I have no idea what JSONable is and you definitely do not need it. Introduce some base class, let’s call it Base:

class Base
  def to_h
    instance_variables.map |iv|
      value = instance_variable_get(:"@#{iv}")
      [
        iv.to_s[1..-1], # name without leading `@`
        case value
        when Base then value.to_h # Base instance? convert deeply
        when Array # Array? convert elements
          value.map do |e|
            e.respond_to?(:to_h) ? e.to_h : e
          end
        else value # seems to be non-convertable, put as is
        end
      ]
    end.to_h
  end
end

Now just derive your classes from Base to make them respond to to_h, define all your instance variables as you did, and call:

require 'json'
JSON.dump request.to_h # request.to_h.to_json should work as well

The above should produce the nested JSON, hashes are happily converted to json by this library automagically.

Comments