Using custom to_json method in nested objects

Viewed 11927

I have a data structure that uses the Set class from the Ruby Standard Library. I'd like to be able to serialize my data structure to a JSON string.

By default, Set serializes as an Array:

>> s = Set.new [1,2,3]
>> s.to_json
=> "[1,2,3]"

Which is fine until you try to deserialize it.

So I defined a custom to_json method:

class Set
  def to_json(*a)
    {
      "json_class" => self.class.name,
      "data" => {
        "elements" => self.to_a
      }
    }.to_json(*a)
  end

  def self.json_create(o)
    new o["data"]["elements"]
  end
end

Which works great:

>> s = Set.new [1,2,3]
>> s.to_json
=> "{\"data\":{\"elements\":[1,2,3]},\"json_class\":\"Set\"}"

Until I put the Set into a Hash or something:

>> a = { 'set' => s }
>> a.to_json
=> "{\"set\":[1,2,3]}"

Any idea why my custom to_json doesn't get called when the Set is nested inside another object?

3 Answers

Here is my approach to getting to_json method for custom classes which most probably wouldn't contain to_a method (it has been removed from Object class implementation lately)

There is a little magic here using self.included in a module. Here is a very nice article from 2006 about module having both instance and class methods http://blog.jayfields.com/2006/12/ruby-instance-and-class-methods-from.html

The module is designed to be included in any class to provide seamless to_json functionality. It intercepts attr_accessor method rather than uses its own in order to require minimal changes for existing classes.

module JSONable
  module ClassMethods
    attr_accessor :attributes

    def attr_accessor *attrs
      self.attributes = Array attrs
      super
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
  end

  def as_json options = {}
    serialized = Hash.new
    self.class.attributes.each do |attribute|
      serialized[attribute] = self.public_send attribute
    end
    serialized
  end

  def to_json *a
    as_json.to_json *a
  end
end


class CustomClass
  include JSONable
  attr_accessor :b, :c 

  def initialize b: nil, c: nil
    self.b, self.c = b, c
  end
end

a = CustomClass.new(b: "q", c: 23)
puts JSON.pretty_generate a

{
  "b": "q",
  "c": 23
}
Related