Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
551 views
in Technique[技术] by (71.8m points)

ruby - Using custom to_json method in nested objects

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...