Rails, json_serializer, associations, and you

While the JSON support in Rails has improved, at least as of 2.1.0 the serializer doesn’t take into account model overrides of #to_json. Fortunately, a patch has been introduced, although it has been staled out.

Rails 2.3.3 and possibly earlier versions of Rails 2.3 no longer have this issue.

In the meantime, I have been defining a class_inheritable_accessor called decorator_methods, calling it from within Serializer.

module ActiveRecord #:nodoc:
  module Serialization
    class Serializer #:nodoc:
      def serializable_decorator_methods
        decorator_methods = nil
        if @record.respond_to?(:decorator_methods)
          decorator_methods = @record.decorator_methods
        end
        Array(decorator_methods)
      end
 
      def serializable_names
        serializable_attribute_names + serializable_method_names + serializable_decorator_methods
      end
    end
  end
end

The same information is reused when I generate each Ext.data.Record with a rake task, allowing a proper record defintion for reading the decorated method with ExtJS.

Or, the original solution to ensure the aforementioned patch works with Rails versions earlier than 2.3:

Another unaddressed issue thus far is the awfulness of the to_json options hash, although the above patch largely allows the workaround of simply defining the necessary options on included associations in the respectively models instead.

For the inclusion of nested associations, you’d want something crazy like the following:

Model.to_json(:include => {:phones => {}, :employer => {:include => {:company => {}}}})

No, you can’t use an array if you need to represent any options at any level. The earlier link to the json_serializer.rb patch allows you override each model’s #to_json to largely avoid the nasty syntax above. Moreover, the options ought to be defined in their respective models anyway, where it makes most sense.

Why else might you care if #to_json overrides on each association are respected? You might want to send out records and their associations in JSON to, say, a Web client running Ext JS, which has its own Ext.data.Record class supporting associations. A record and all its relevant associations are then available on the client to be manipulated.

As it happens, I did have to modify the patch to correctly include missing commas between associations and to remove extra braces inserted unnecessarily. The JSON produced is now valid. I have not roboustly tested it. David Burger gets full credit for this patch.

--- /var/lib/gems/1.8/gems/activerecord-2.1.0/lib/active_record/serializers/json_serializer.rb   2008-07-20 17:04:47.000000000 -0400
+++ json_serializer.rb  2008-12-14 16:21:40.000000000 -0500
@@ -66,8 +66,25 @@
     end
 
     class JsonSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
+      def add_associations(association, records, opts, json)
+        if records.is_a?(Enumerable)
+          include_json = ''
+          if !records.empty?
+            include_json = records.map { |record| record.to_json(opts) }.join(', ')
+          end
+          json << "\"#{association.to_s}\": [#{include_json}]"
+        else
+          json << "\"#{association.to_s}\": #{records.to_json(opts)}"
+        end
+      end
+
       def serialize
-        serializable_record.to_json
+        include_json = ''
+        association_json = []
+        add_includes { |association, records, opts| add_associations(association, records, opts, association_json) }
+        include_json << ", #{association_json.join(', ')}" unless association_json.blank?
+        json = serializable_names.inject({}) { |names, name| names[name] = @record.send(name); names }.to_json
+        "#{json[0..-2]}#{include_json}}"
       end
     end

The final result is something like the following.

def to_json(options={})
  options = options.merge(:include => [:phones, :employer])
  super(options)
end
 
>> ActiveSupport::JSON.decode(Person.first.to_json(:only => :id))
=> {"employer"=>{"id"=>1144}, "id"=>656, "phones"=>[{"id"=>109}, {"id"=>80}]}

All of this is building up to being able to do the following, so all formatting logic is local instead of being done via Ext JS on the client.

class Phone < ActiveRecord::Base
  belongs_to :phone_type
...
  def to_json
    super(:methods => [:phone_number], :include => [:phone_type])
  end
 
  def phone_number
    "(#{area_code}) #{dashize(number)}"
  end
 
  def dashize(number)
    ...
  end
end

Afterwards, data can be decorated the same whether the data is delivered over a JSON API or directly to a browser via server rendered HTML or even as a PDF.

It’s important to note that if @phone_type#to_json has an include for :phone, there will be infinite recursion because @phone#to_json then includes :phone_type, which might be why the patch was never applied to core.

One Comment

  1. hade
    Posted 8/28/2009 at 3:30 am | Permalink

    Thanks for the post - was REALLY helpful!!!

Post a Comment

Your email is never shared. Required fields are marked *

*
*