Using custom to_json method in nested objects

后端 未结 3 713
佛祖请我去吃肉
佛祖请我去吃肉 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:39

    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
    }
    
    0 讨论(0)
  • 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.

    0 讨论(0)
  • 2020-12-05 12:54

    Looking for a solution on the same problem, i found this bug report on the Rails issue tracker. Besides it was closed, i suppose it still happens on the earlier versions. Hope it could help.

    https://github.com/rails/rails/issues/576

    0 讨论(0)
提交回复
热议问题