ruby-on-railsassociationsactive-model-serializersactivemodel

Picking specific fields for an active model serializer association


In Active Model Serializers.. Let's say I have CompanySerializer and EmployeeSerializer. On CompanySerializer I have a field ceo where I want to render the association with EmployeeSerializer but only show a subset of the fields from EmployeeSerializer.

For the purposes of this example, let's say that EmployeeSerializer has the attributes name, homepage, and salary, but on CompanySerializer.ceo we only want to show name and homepage.

I would like to avoid redefining a new CompanyEmployeeSerializer class, if I can just "pick" the fields that I want for the employee association inside CompanySerializer.

I also want to avoid a solution that runs EmployeeSerializer then slices for specific fields. The reason for this is I want to avoid running the code associated with serializing the non-desired fields.

Note: this is different than the question of how you hide certain fields inside a serializer based on a condition. This is about how you can call a serializer from another serializer and get specific fields.

If something like this existed, I'd be quite happy:

CompanySerializer < ActiveModel::Serializer
  belongs_to :ceo, serializer: EmployeeSerializer, fields: [:name, :homepage]
end

Solution

  • This is as close as I could get:

    # app/serializers/company_serializer.rb
    
    class CompanySerializer < ActiveModel::Serializer
      belongs_to :ceo do
        EmployeeSerializer.new(object.ceo).attributes([:name, :homepage])
      end
    end
    

    Could you do it this way?

    # app/serializers/company_serializer.rb
    
    class CompanySerializer < ActiveModel::Serializer
      belongs_to :ceo, serializer: EmployeeSerializer, fields: [:name]
    end
    

    You'd have to dig deep:

    $ git diff
      diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb
      index 5e34779..e50f44f 100644
      --- a/lib/active_model/serializer.rb
      +++ b/lib/active_model/serializer.rb
      @@ -367,7 +367,9 @@ module ActiveModel
             adapter_options ||= {}
             options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
             if (fieldset = adapter_options[:fieldset])
      -        options[:fields] = fieldset.fields_for(json_key)
      +        if fields = fieldset.fields_for(json_key)
      +          options[:fields] = fields
      +        end
             end
             resource = attributes_hash(adapter_options, options, adapter_instance)
             relationships = associations_hash(adapter_options, options, adapter_instance)
      diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb
      index 7aeee33..ce0cd29 100644
      --- a/lib/active_model/serializer/association.rb
      +++ b/lib/active_model/serializer/association.rb
      @@ -55,7 +55,8 @@ module ActiveModel
               association_object = association_serializer && association_serializer.object
               return unless association_object
    
      -        serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
      +        options = {fields: reflection.options[:fields]}.compact
      +        serialization = association_serializer.serializable_hash(adapter_options, options, adapter_instance)
    
               if polymorphic? && serialization
                 polymorphic_type = association_object.class.name.underscore
    

    Monkey patch to give it a try:

    # config/initializers/serializer_patch.rb
    
    ActiveModel::Serializer::Association.class_eval do
      def serializable_hash(adapter_options, adapter_instance)
        association_serializer = lazy_association.serializer
        return virtual_value if virtual_value
        association_object = association_serializer && association_serializer.object
        return unless association_object
    
        # pass `fields:` option from the reflection to serializer
        options = {fields: reflection.options[:fields]}.compact
        serialization = association_serializer.serializable_hash(adapter_options, options, adapter_instance)
    
        if polymorphic? && serialization
          polymorphic_type = association_object.class.name.underscore
          serialization = {type: polymorphic_type, polymorphic_type.to_sym => serialization}
        end
        serialization
      end
    end
    
    ActiveModel::Serializer.class_eval do
      def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
        adapter_options ||= {}
        options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
        if (fieldset = adapter_options[:fieldset])
    
          # fix controller `fields:` always overriding our patched fields option 
          # even if you don't pass `fields:` to `render`:
          if fields = fieldset.fields_for(json_key)
            options[:fields] = fields
          end
    
        end
        resource = attributes_hash(adapter_options, options, adapter_instance)
        relationships = associations_hash(adapter_options, options, adapter_instance)
        resource.merge(relationships)
      end
    end
    

    Test:

    >> CompaniesController.renderer.render(json: Company.first)
    => "{\"id\":1,\"name\":null,\"ceo\":{\"name\":null}}"