active_model_serializers icon indicating copy to clipboard operation
active_model_serializers copied to clipboard

Serializer upgrade question - options (:meta & :fields) in v0.9.x and v0.10.x

Open agalloch opened this issue 7 years ago • 2 comments

Hello AMS team, 👋 🙂

First of all, thank you for maintaining this little gem!

It's not a gem issue, rather an upgrade question and I would really appreciate any feedback.

We've got a Rails 4 app built with AMS v0.9.x and we need to upgrade it to Rails 5. We're not using many of the advanced AMS features (yet) but rely on 2 features that had changed since v0.10.x - the meta method and response filtering with :only/:except options.

Following examples and questions assume Attributes adapter is being used in v0.10.x code.

options[:meta]

I'm not sure of the original purpose of :meta but reading v0.10.x docs and stepping through the code, it seems to now be meant specifically for rendering a metadata key in response JSON. Besides that usage we also pass common serialization context to our serializers with it.

For example:

class BillSerializer < ActiveModel::Serializer
  attributes :id, :paid_amount, :currency

  def currency
    # This is the same currency for all bills; could be expensive to fetch per item
    meta[:currency].name
  end 
end

class CustomCollectionSerializer < ActiveModel::Serializer
  attributes :data

  has_one :pagination, serializer: PaginationSerializer

  def data
    object.elements.map do |element|
      serializer.new(element, {meta: meta}).serializable_hash
    end
  end

  def serializer
    @serializer ||= object.elements_serializer
  end
end

# controller code
render json: bills, 
  meta: {currency: common_currency}, 
  each_serializer: BillSerializer,
  serializer: CustomCollectionSerializer

produces this JSON:

# API response
{
  "data": [
    {"id": 1, "paid_amount": 25.0, "currency": "USD"},
    {"id": 2, "paid_amount": 37.0, "currency": "USD"},
    # etc.
  ],
  "pagination": {
    "current_page": 1,
    "total_pages": 20,
    "per_page": 10,
    "total_entries": 199
  }
}

I can emulate the old behaviour using a custom key (say, :old_meta) but I wonder if there's a supported option which is more appropriate for passing in serialization context?

options[:fields]

The other problem stems from the same custom collection serializer above - namely, passing :only/:except (white-/blacklist) to a collection item:

class CustomCollectionSerializer < ActiveModel::Serializer
  def initialize(resource, options = {})
    @element_options = options.slice :only

    # We don't want CustomCollectionSerializer's attributes to be filtered ever.
    super object, options.except(:only)
  end

  def data
    object.elements.map do |element|
      serializer.new(element, {meta: meta}.merge(@element_options)).serializable_hash
    end
  end

  # rest is as above but omitted for brevity
end

In v0.10.x there is only :fields (a whitelist) and it's the adapter's responsibility to apply it. When I just pass it in the controller (with approriate rename of :only key to :fields):

render json: bills, 
  fields: [:paid_amount], 
  each_serializer: BillSerializer,
  serializer: CustomCollectionSerializer

instead of the correct JSON (i.e. filtered item attributes):

# API response
{
  "data": [
-   {"id": 1, "paid_amount": 25.0, "currency": "USD"},
+   {"paid_amount": 25.0},
-   {"id": 2, "paid_amount": 37.0, "currency": "USD"},
+   {"paid_amount": 37.0},
    # etc.
  ],
  "pagination": {
    "current_page": 1,
    "total_pages": 20,
    "per_page": 10,
    "total_entries": 199
  }
}

I get this (no "data" attribute):

# API response
{
- "data": [
-   {"id": 1, "paid_amount": 25.0, "currency": "USD"},
-   {"id": 2, "paid_amount": 37.0, "currency": "USD"},
- ],
  "pagination": {
    "current_page": 1,
    "total_pages": 20,
    "per_page": 10,
    "total_entries": 199
  }
}

due to the adapter checking CustomCollectionSerializer#data attribute against the whitelist (fields: [:paid_amount]) and filtering it out. With v0.9.x it was possible to apply the whitelist only to collection item serializer but I can't figure out how to do it with the adapter in v0.10.x.

So, in summary, my questions are:

  1. What's the next best option to use instead of :meta to pass serialization context to a collection?
  2. How to properly whitelist custom collection item attributes with :fields?

P.S. I've checked around other issues and found this comment https://github.com/rails-api/active_model_serializers/issues/1864#issuecomment-238826170 but would like to avoid implementing one more never-to-be-supported-upstream feature (like writing a custom adapter class) if possible.

agalloch avatar Aug 08 '18 13:08 agalloch

I believe this would be fixed by https://github.com/rails-api/active_model_serializers/pull/2223

bf4 avatar Aug 10 '18 18:08 bf4

Thank you for the response, @bf4! 🙂

The linked PR seems to deal with options[:fields] which is nice (haven't tried it yet); any idea when will it be released?

What about options[:meta] - do you plan to add support for it in :attributes adapter?

agalloch avatar Aug 13 '18 17:08 agalloch