Serializing Deeply Nested Associations

Posted by rhhaynes on September 17, 2018

While it’s generally preferred to keep data layers as shallow as possible, at some point when building APIs it may be necessary to include attributes spanning multiple relationships and levels. Unfortunately the default nesting for json with active_model_serializers in Rails is only one layer. This blog post aims to address this issue by exploring a couple ways to serialize deeply nested associations.

Single-layer nesting by default

To illustrate the problem, imagine an API that provides geolocation data (i.e., latitutde and longitude values) for hurricanes. In addition to actual hurricane positions, a user may be interested in the geolocations of all associated forecasts, or spaghetti models. Behind the scenes one way to associate such information might be as follows.

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
class Geolocation < ApplicationRecord
  belongs_to :latlng, :polymorphic => true
end
class Hurricane < ApplicationRecord
  has_many :geolocations, :as => :latlng
  has_many :spaghetti_models
end
class SpaghettiModel < ApplicationRecord
  belongs_to :hurricane
  has_many :geolocations, :as => :latlng
end

Likewise the corresponding serializers using active_model_serializers are shown below.

class GeolocationSerializer < ActiveModel::Serializer
  attributes :lat, :lng
  belongs_to :latlng, :polymorphic => true
end
class HurricaneSerializer < ActiveModel::Serializer
  attributes :name, :category
  has_many :geolocations, :as => :latlng
  has_many :spaghetti_models
end
class SpaghettiModelSerializer < ActiveModel::Serializer
	belongs_to :hurricane
	has_many :geolocations, :as => :latlng
end

Unfortunately using this setup, the API output for the 2018 Atlantic hurricane season would look as follows. As you can see, no geolocation data is contained under spaghetti_models.

[
  {
    "name": "Hurricane Florence",
    "category": 4,
    "geolocations": [ {"lat": 12.9, "lng": -18.4}, {"lat": 12.9, "lng": -19.0}, ... ],
    "spaghetti_models": [ {}, {}, ... ]
  },
  ...
]

Deeply nested serializations

To achieve the desired API output shown below, one of the following two approaches can be taken.

[
  {
    "name": "Hurricane Florence",
    "category": 4,
    "geolocations": [ {"lat": 12.9, "lng": -18.4}, {"lat": 12.9, "lng": -19.0}, ... ],
    "spaghetti_models": [
      {
        "name": "forecast_1",
        "geolocations": [ {"lat": 12.9, "lng": -18.4}, {"lat": 13.2, "lng": -20.1}, ... ]
      },
      {
        "name": "forecast_2",
        "geolocations": [ {"lat": 12.9, "lng": -19.4}, {"lat": 13.1, "lng": -20.5}, ... ]
      },
      ...
    ]
  },
  ...
]

Deep-nesting by default

To change the default nesting level, a configuration property can be set in an initializer for active_model_serializers. Simply create config/initializers/active_model_serializer.rb and reset the default_includes for ActiveModel::Serializer.

ActiveModel::Serializer.config.default_includes = '**' # (default '*')

It should be noted that problems with infinite recursion can occur using this approach unless care is taken.

Customizing serializer behavior

A safer approach to achieving the desired nesting depth involves disregarding the has_many, belongs_to relationships in the serializer and defining the nested association directly. In this way the serializer behavior can be customized to provide the select attributes.

class SpaghettiModelSerializer < ActiveModel::Serializer
  attributes :name, :geolocations

  def geolocations
    object.geolocations.collect do |geolocation|
      { :lat => geolocation.lat, :lng => geolocation.lng }
    end
  end
end