Zeeshan
Zeeshan

Reputation: 3024

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:

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

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

Other Info:

Upvotes: 1

Views: 1602

Answers (1)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121020

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 do |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.

Upvotes: 2

Related Questions