SBaronda.com a little place I like to call home.


Dynamic ActiveModelSerializers

Jan. 25, 2023

I've used ActiveModelSerializers (AMS) for a long time. It's development history is unique as there are 3 different major branches of it with breaking changes per branch. I don't recommend that you use AMS anymore as there isn't a ton of development happening around it. I'm mostly writing this tutorial because I keep re-explaining this solution to work folk.

In this tutorial, I'll show you how to add direct or low-level control over you attributes as a way to present every method on your object be available as attributes.

This is a good starting point, but this is what we want to serialize on.

class Product
  attr_accessor :data
  def new(data)
    @data = data
  end

  def presenting_method
    @data[:some_key]
  end

  def read_attribute_for_serialization(n)
    @data[n]
  end
end

In this case, we want to serialize present_method as well as a majority of all the other methods within the Product class.

We have a couple different ways of doing it with different nuances per method.

Doing the manual mapping

This one is easy and works great if you want to proxy < 15 or so methods. This one is also important if you want to make sure you don't overexpose methods from your class that you want to serialize.

class ProductSerializer < ActiveModel::Serializer
  attributes :presenting_method

  def presenting_method
    object.presenting_method
  end
end

Doing it with delegators and endless functions

Delegators or endless functions make writing the above more effective, but typically delegators are the preferred method. In this example, I'll use Ruby On Rails delegate class for delegation. If you aren't depending on Ruby on Rails then you can use these two methods from the Ruby stdlib: Forwardable or Delegator. If you are interested in these methods please checkout this blogpost by AppSignal on the topic.

With endless functions:

class ProductSerializer < ActiveModel::Serializer
  attributes :presenting_method

  def presenting_model = object.presending_method
end

Now with delegators:

class ProductSerializer < ActiveModel::Serializer
  attributes :presenting_method

  delegate :presenting_method, to: :object

  # if you had more than one method you want to proxy or delegate to an object you
  # can do so by specifying multiple methods:
  #
  #   delegate :presenting_method, :another_method, :meth1, :meth2, to: :object
end

Full send (meta-programming)

In this last example, I'll show you how to do the same thing via meta-programming. Note: this is dangrous as it'll likely expose more than you are interested in.

The trick to making this work is available within the README.md for AMS 0.8. This will be the basis of what we'll be working with for this example.

class ProductSerializer < ActiveModel::Serializer
  attributes :first_name, :last_name

  def attributes
    hash = super
    if current_user.admin?
      hash["ssn"] = object.ssn
      hash["secret"] = object.mothers_maiden_name
    end
    hash
  end
end

In this case, we'll just need to build out a hash of serialized attribute => value.

The first step will be getting the respectful methods we want to convert to serialized attributes through meta-programming:

> Product.new(test: 'hello')
    .public_methods(false)
    .excluding(:data, :data=, :read_attribute_for_serialization)
# [:presenting_model]

Using the above example through the REPL now we can finish the example by using our meta-programming above within the serializer:

class ProductSerializer < ActiveModel::Serializer
  def attributes
    hash = super

    object
      .public_methods(false)
      .excluding(:data, :data=, :read_attribute_for_serialization)
      .each do |attribute|
        hash[attribute] = object.send(attribute)
      end

    hash
  end
end


comments powered by Disqus