The first chunk is for Rails 3.1 (older versions will be pretty much the same); the second chunk is for the standard non-Rails JSON. Skip to the end if tl;dr.
Your problem is that Rails does this:
[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
klass.class_eval <<-RUBY, __FILE__, __LINE__
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
RUBY
end
in active_support/core_ext/object/to_json.rb
. In particular, that changes Hash's to_json
method into just an ActiveSupport::JSON.encode
call.
Then, looking at ActiveSupport::JSON::Encoding::Encoder
, we see this:
def encode(value, use_options = true)
check_for_circular_references(value) do
jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
jsonified.encode_json(self)
end
end
So all the Rails JSON encoding goes through as_json
. But, you're not defining your own as_json
for Set, you're just setting up to_json
and getting confused when Rails ignores something that it doesn't use.
If you set up your own Set#as_json
:
class Set
def as_json(options = { })
{
"json_class" => self.class.name,
"data" => { "elements" => self.to_a }
}
end
end
then you'll get what you're after in the Rails console and Rails in general:
> require 'set'
> s = Set.new([1,2,3])
> s.to_json
=> "{"json_class":"Set","data":{"elements":[1,2,3]}}"
> h = { :set => s }
> h.to_json
=> "{"set":{"json_class":"Set","data":{"elements":[1,2,3]}}}"
Keep in mind that as_json
is used to prepare an object for JSON serialization and then to_json
produces the actual JSON string. The as_json
methods generally return simple serializable data structures, such as Hash and Array, and have direct analogues in JSON; then, once you have something that is structured like JSON, to_json
is used to serialize it into a linear JSON string.
When we look at the standard non-Rails JSON library, we see things like this:
def to_json(*a)
as_json.to_json(*a)
end
monkey patched into the basic classes (Symbol, Time, Date, ...). So once again, to_json
is generally implemented in terms of as_json
. In this environment, we need to include the standard to_json
as well as the above as_json
for Set:
class Set
def as_json(options = { })
{
"json_class" => self.class.name,
"data" => { "elements" => self.to_a }
}
end
def to_json(*a)
as_json.to_json(*a)
end
def self.json_create(o)
new o["data"]["elements"]
end
end
And we include your json_create
class method for the decoder. Once that's all properly set up, we get things like this in irb
:
>> s = Set.new([1,2,3])
>> s.as_json
=> {"json_class"=>"Set", "data"=>{"elements"=>[1, 2, 3]}}
>> h = { :set => s }
>> h.to_json
=> "{"set":{"json_class":"Set","data":{"elements":[1,2,3]}}}"
Executive Summary: If you're in Rails, don't worry about doing anything with to_json
, as_json
is what you want to play with. If you're not in Rails, implement most of your logic in as_json
(despite what the documentation says) and add the standard to_json
implementation (def to_json(*a);as_json.to_json(*a);end
) as well.