Active_model_serializers: Respect controller namespace when looking up serializers

Created on 7 Aug 2012  Â·  18Comments  Â·  Source: rails-api/active_model_serializers

I have name-spaced my API controllers like so:

module EngineName
  class Post < ActiveRecord::Base; end

  class API::V1::APIController < ActionController::API; end

  class API::V1::PostsController < API::V1::APIController
    def index
      @posts = Post.all
      render json: @posts
    end
  end
end

Since each API version might serialize the models differently, ideally the serializers should be namespaced accordingly like so:

module EngineName
  class Post < ActiveRecord::Base; end

  class API::V1::PostSerializer < ActiveModel::Serializer
    attributes :id, :title, :body
  end

  class API::V1::APIController < ActionController::API; end

  class API::V1::PostsController < API::V1::APIController
    def index
      @posts = Post.all
      render json: @posts, serializer: PostSerializer # resolves to ::EngineName::API::V1::PostSerializer
    end
  end
end

This gets quite repetitive when you have to do it for all your controllers. I am wondering if it is possible to take into account of the controller's scope when looking up a serializer. In the example above, render json: @posts should these in order (via controller.class.const_get :PostSerializer):

::EngineName::API::V1::PostsController::PostSerializer
::EngineName::API::V1::PostSerializer
::EngineName::API::PostSerializer
::EngineName::PostSerializer
::PostSerializer

(I also thought about overriding default_serializer_options in APIController, but that won't work as the options are merged _after_ the serializer lookup, so def default_serializer_options; { serializer: SomeSerializer }; end won't work.)

Most helpful comment

Anyone coming across this issue in the brave new world of 2018 should look here:

All 18 comments

With #114, you can now work around this by doing this:

class ApplicationController < ActionController::Base
  def default_serializer_options
    serializer_name = (self.class.name.demodulize.sub(/Controller$/,'').singularize + 'Serializer').to_sym
    serializer = self.class.const_get serializer_name

    if serializer
      super.merge(serializer: serializer)
    else
      super
    end
  end
end

But this is obviously very hackish (it relies on controller name matching the model), so I'm still looking for a better solution. Perhaps make active_model_serializer (the method on the model) take an optional "lookup_context" argument?

Has anything happened with this?

Nope. I think it has to do with this is just pretty damn hard to implement cleanly..

Sent from my phone

On 2012-10-04, at 7:41 PM, Christopher Bull [email protected] wrote:

Has anything happened with this?

—
Reply to this email directly or view it on GitHub.

Yep. I don't see how we can support something like this in a clean manner. If you want to do crazy nesting, you should figure out how to handle your complexity. I don't think we can support this effectively in the general case.

I've been looking to integrate this into our app (versioned api), but haven't been able to because of the name spacing of the controllers. Has there been any effort to revise this? Am I correct in understanding that the intent is to version models with the versioned controller API? If so, then this gem specifically excludes the concept of versioning your API...? Open to revisions?

This gem specifically does nothing about versioning.

Serializers are supposed to be 1-Many with models, not 1-1. Make two different serializers for two different versions.

@chancancode hi awesome job on the PR!

Looks like the ability to call super.merge was removed in the latest. Do you recommend another workaround for adopting this kind of versioning support?

@chourobin Why? As far as I can this should still work. The default implementation of default_serializer_options is empty so you'll have to do (super || {}).merge( ... ), but other than that if it doesn't work you probably ran into a bug.

@chancancode : Thank you for the trick.

However when dealing with Mongoid::Criteria it seems to be harder than that.

First, I was previously obliged to use these lines to support Criteria :

Mongoid::Document.send(:include, ActiveModel::SerializerSupport)
Mongoid::Criteria.delegate(:active_model_serializer, to: :to_a)

It worked, but then, since I introduced the default_serializer_options trick, all these workarounds lamentably started to fail and I always get :

V1::UserSerializer is not an ArraySerializer. You may want to use the :each_serializer option instead.

Hello, we have some methods to support serializers namespace.

I don't know if it is a pretty solution but temporary it works.

I hope it helps.

class ApplicationController < ActionController::Base

  private

  def default_serializer_options
    {
      serializer_key => serializer
    }
  end

  def namespace
    self.class.to_s.deconstantize
  end

  def serializer_key
    single_action? ? :serializer : :each_serializer
  end

  def serializer
    namespaced_serializer || active_model_serializer || default_serializer
  end

  def namespaced_serializer
    "#{namespace}::#{serializer_name}".constantize rescue nil
  end

  def active_model_serializer
    serializer_name.constantize rescue nil
  end

  def default_serializer
    ActiveModel::DefaultSerializer
  end

  def serializer_name
    "#{send(:resource_class).model_name}Serializer"
  end

  def single_action?
    !collection_actions.include?(params[:action])
  end

  def collection_actions
    %w(index)
  end

end

Cool !
I hacked a bit on your code and I managed to get it worked for me.
Thank you @franciscodelgadodev

:D you're welcome.

won't this break for has_one and has_many (where the serializer name is assumed from the object)?

in other words, what happens if you have

class V1::PostSerializer < ActiveModel::Serializer
  has_many :comments #will it know the correct version to use?
end

class V1::CommentSerializer < ActiveModel::Serializer
end

@chancancode @franciscodelgadodev @chourobin

By using default_serializer_options to manually override the serializer name you also create a side effect. Imagine that you have something like that in your ApplicationController :

  rescue_from(ActionController::ParameterMissing) do |exception|
    error = {}
    error[exception.param] = ['parameter is required']
    response = { errors: [error] }
    render json: response, status: :unprocessable_entity
  end

(JSON rendering on exceptions raised by strong_parameters gem).

If the following exception is raised from, let's say, V1::UsersController, then default_serializer_options method will use V1::UserSerializer because of your hack, rather than just returning false on that portion of code :

      def build_json(controller, resource, options)
        default_options = controller.send(:default_serializer_options) || {}
        options = default_options.merge(options || {})

        serializer = options.delete(:serializer) ||
          (resource.respond_to?(:active_model_serializer) &&
           resource.active_model_serializer)

        return serializer unless serializer

which leads to that kind of errors :

NoMethodError (undefined method `read_attribute_for_serialization' for {:errors=>[{:auth_token=>["parameter is required"]}]}:Hash):

Side effects could be even worse if you have an ErrorsController whose responsibility is to render JSON errors on 404, 422, 500 ...

That's sad because, indeed, it would be interesting to respect controller namespace when looking up serializers.

If some people here have (safe enough) solutions to add this behaviour to AMS, please share. :heart: .

You can manually render the response by replacing render json: response, status: :unprocessable_entity with render json: response.to_json, status: :unprocessable_entity.

That said, I think AM::S could provide some better hooks for this purpose. I started a new fork lately to experiment with that concept, my goal is to eventually propose those changes here when I figured it out and hopefully have that merged upstream.

@chancancode

I had already tried to do a render json: response.to_json, status: :unprocessable_entity in the previous rescue_from block, however it still uses AMS, calls default_serializer_options method and I end up with the same read_attribute_for_serialization undefined method error.

But it works if I do the following (and ugly) thing :
render json: response, serializer: nil

That said, I think AM::S could provide some better hooks for this purpose. I started a new fork lately to experiment with that concept, my goal is to eventually propose those changes here when I figured it out and hopefully have that merged upstream.

Awesome ! ;)

PS : I'm on 0.8.1 version of AMS.

Anyone coming across this issue in the brave new world of 2017 should look here: https://github.com/rails-api/active_model_serializers/blob/master/docs/general/rendering.md#namespace

Anyone coming across this issue in the brave new world of 2018 should look here:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Andriykoo picture Andriykoo  Â·  5Comments

yjukaku picture yjukaku  Â·  5Comments

iggant picture iggant  Â·  4Comments

AlexCppns picture AlexCppns  Â·  5Comments

attenzione picture attenzione  Â·  4Comments