Search code examples
ruby-on-railsruby-on-rails-5active-model-serializers

Accessing Parent object in Child serializer in ActiveModelSerializers


Presenting a hypothetical, simplified version of my problem. Imagine I have a website where users can create a Shop with their own branding and choose from a catalog of Products to show in their Shop. Shops & Products have a has-and-belongs-to-many (HABTM) relationship. Each Product has its own Shop-specific route.

Rails.application.routes.draw do
  resources :shops do
    resources :products
  end
end

class ShopSerializer < ActiveModel::Serializer
  has_many :products
end

class ProductSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers

  attribute :url do
    shop_product_url(NEED SHOP ID, product_id: object.id)
  end  
end

When a Shop is serialized, and as a result, so is the collection of its Products, I want the Product serializer to be aware of the shop that is serializing it and use that to include the route in the serialized output. How is this possible? I've tried all manner of passing instance_options from the ShopSerializer but it doesn't work as expected.

# this works except is apparently not threadsafe as multiple
# concurrent requests lead to the wrong shop_id being used
# in some of the serialized data
has_many :products do
  ActiveModelSerializers::SerializableResource.new(shop_id: object.id).serializable_hash
end

# shop_id isn't actually available in instance_options
has_many :products do
  ProductSerializer.new(shop_id: object.id)
end

Solution

  • Unfortunately serializer associations do not seem to provide a clean way to pass in custom attributes to the child serializers. There are a few not-so-pretty solutions though.

    1. Invoke ProductSerializer manually, add URL in ShopSerializer

    class ProductSerializer < ActiveModel::Serializer
    end
    
    class ShopSerializer < ActiveModel::Serializer
      include Rails.application.routes.url_helpers
    
      attribute :products do
        object.products.map do |product|
          ProductSerializer.new(product).serializable_hash.merge(
            url: shop_product_url(object.id, product_id: product.id)
          )
        end
      end
    end
    

    2. Add shop ID to the Product instances before they are fed to ProductSerializer

    class ProductSerializer < ActiveModel::Serializer
      include Rails.application.routes.url_helpers
    
      attribute :url do
        shop_product_url(object.shop_id, product_id: object.id)
      end 
    end
    
    class ShopSerializer < ActiveModel::Serializer
      has_many :products, serializer: ProductSerializer do
        shop = object
        shop.products.map do |product|
          product.dup.tap do |instance|
            instance.singleton_class.send :define_method, :shop_id do
              shop.id
            end
          end
        end
      end
    end
    

    Both solutions should be thread safe, but the first solution seems like a better idea to me, as the second one makes ProductSerializer unusable on its own — i.e. when just a single Product is serialized without knowing the particular shop it should belong to.