Using custom to_json method in nested objects

后端 未结 3 717
佛祖请我去吃肉
佛祖请我去吃肉 2020-12-05 12:06

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

3条回答
  •  春和景丽
    2020-12-05 12:53

    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.

提交回复
热议问题