Manage multiple versions of JSON serializers in a Rails application with ease.

Aside: Even though Rails has an inbuilt mechanism to serialize domain objects, ActiveModel Serializers (AMS) is a popular library for decoupling the serialization logic from database layer. This post assumes usage of AMS.

Change is the only constant in software development and it is the view or presentation layer that changes the most. In case of API-only apps, JSON schema is the view.

Assume we have resources like product and inventory with corresponding serializers.

Now product info is also required in inventory details. Using belongs_to association would pick the main product serializer which would render the entire product details which we don't need. So we create another serializer InventoryProductSerializer because adding some method with hand-rolled json is not a good practice - which is exactly why we use serializers in the first place.

Then we also have delivery_order - which also needs product info but with slightly different fields. We think of saving those precious bytes on the network by sending only required fields and not single more. So we religiously create DeliveryOrderProductSerializer. This goes on and on...

product_serializer.rb
inventory_product_serializer.rb
delivery_order_product_serializer.rb

Lets take a moment and see if we can reduce this. Ideally inventory-product serializer will be used **only** by inventory serializer. So why not somehow put it inside InventorySerializer where it belongs?

We can conveniently take advantage of Ruby's constant lookup algorithm to do this:

class InventorySerializer < ActiveModel::Serializer
  attributes :id, :location, :quantity
  has_one :product

  # Return only required attributes here to keep API small
  class ProductSerializer < ActiveModel::Serializer
    attributes :id, :name, :price
  end
end
class DeliveryOrderSerializer < ActiveModel::Serializer
  attributes :id, :quantity, :price, :source, :destination
  has_one :product

  # Return only required attributes here to keep API small
  class ProductSerializer < ActiveModel::Serializer
    attributes :id, :name
  end
end

While looking up for any constant (here the class constant ProductSerializer), it is first looked up inside the serializer class itself. We have one there and which gets used. The next lookup of outer global ProductSerializer in product_serializer.rb never happens in this case.

Thus we have achieved colocated concerns. Hope this helps.

Happy hacking!