20

I'm using active_model_serializer. Now I want to serialize an object with pagination, should I do the pagination logic in the controller or in the serializer?

If I choose to do the pagination in serializer, I need to pass the page_number and per_page to the serializer. How should I do that? My understanding is serializer only takes the model object.

Bruce Lin
  • 2,700
  • 6
  • 28
  • 38
  • Really unclear where serialization comes into this. What are you serializing? How does it relate to pagination? The two are *completely* separate, I can't imagine what one has to do with the other. – user229044 Apr 08 '14 at 20:42
  • @meagar I'm trying to serialize the album, in which I want to do pagination for photos. – Bruce Lin Apr 08 '14 at 20:50
  • Are your trying to say that your result is an array and will_paginate is not working? – Ruby Racer Apr 08 '14 at 23:13
  • @StavrosSouvatzis Sorry for the confusion. I'm actually using Kaminari and the pagination works well. My question is where to put the pagination logic - shall I put it in controller or shall I put it in serializer. – Bruce Lin Apr 08 '14 at 23:26
  • Just to clarify for future readers, AMS now includes pagination automatically when using either Kaminari or WP gems: https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md – rmcsharry Sep 08 '16 at 19:24
  • 1
    @rmcsharry FYI but Link Rot has set in. I think this page is equivalent: https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/howto/add_pagination_links.md – RonLugge Jun 07 '18 at 19:03

3 Answers3

46

Single Use Solution

Regular serializers are only concerned with single items - not paginated lists. The most straight forward way to add pagination is in the controller:

customers = Customer.page(params[:page])
respond_with customers, meta: {
  current_page: customers.current_page,
  next_page: customers.next_page,
  prev_page: customers.prev_page,
  total_pages: customers.total_pages,
  total_count: customers.total_count
}

Reusable Solution

However, this is pretty tedious if you need pagination logic for multiple objects. Looking through the documentation for active_model_serializers you'll come across an ArraySerializer for serializing an array of objects. What I did was create pagination_serializer.rb using ArraySerializer to automatically add the meta tag for paginated arrays:

# my_app/app/serializers/pagination_serializer.rb
class PaginationSerializer < ActiveModel::Serializer::ArraySerializer
  def initialize(object, options={})
    meta_key = options[:meta_key] || :meta
    options[meta_key] ||= {}
    options[meta_key][:pagination] = {
      current_page: object.current_page,
      next_page: object.next_page,
      prev_page: object.prev_page,
      total_pages: object.total_pages,
      total_count: object.total_count
    }
    super(object, options)
  end
end

Once you have PaginationSerializer added to your rails app, you simple need to call it when you need pagination meta tags from your controller:

customers = Customer.page(params[:page])
respond_with customers, serializer: PaginationSerializer

Note: I wrote this to use Kaminari as the paginator. However, it can easily be modified to work with any pagination gem or custom solution.

equivalent8
  • 13,754
  • 8
  • 81
  • 109
lightswitch05
  • 9,058
  • 7
  • 52
  • 75
  • 1
    This is a pretty tedious and repetitive way to go about it if you have lots of resources that you want to paginate. – Mark Murphy Apr 23 '14 at 13:15
  • @MarkMurphy You are right - very tedious. I'm in the process of moving the pagination details into a serializer of its own and will update my answer as soon as I work out the remaining bugs. Until then you can lookup `ArraySerializer` – lightswitch05 Apr 23 '14 at 19:12
  • 8
    @MarkMurphy I updated my answer to show how to use ArraySerializer to clean up the code and be more DRY – lightswitch05 Jun 13 '14 at 13:55
  • 2
    I wanted the pagination keys to be top level so I also overwrote #as_json on ``PagedSerializer < ActiveModel::ArraySerializer`` def as_json(*args) @options[:hash] = hash = {} @options[:unique_values] = {} hash.merge!(@options[:pagination]) if @options.key?(:pagination) root = @options[:root] if root.present? hash.merge!(root => serializable_array) include_meta(hash) hash else serializable_array end end – aaronmgdr Dec 18 '14 at 20:32
  • Thanks for providing such good solution, but it seems that `ActiveModel::Serializer::ArraySerializer` should be `ActiveModel::ArraySerializer`. – Li Dong Dec 02 '15 at 14:16
  • 1
    @LiDong it depends on what version of `active_model_serializers` you have. 0.10.0 and newer requires `ActiveModel::Serializer::ArraySerializer` - older versions require `ActiveModel::ArraySerializer` – lightswitch05 Dec 02 '15 at 15:22
  • @lightswitch05 Yes, you are right, I am using 0.9.3. – Li Dong Dec 03 '15 at 05:37
  • 2
    Great solution but no longer required, see: https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md – rmcsharry Sep 08 '16 at 19:23
  • 1
    How to add pagination links document changed URL to https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/howto/add_pagination_links.md – Zoran Majstorovic Oct 18 '17 at 12:40
  • As of version 0.10 this strategy won't work anymore due to meta options being moved to the adapter (ie meta isn't handled by CollectionSerializer). So, a similar approach can be used by creating an adapter (for example the JsonApi adapter provided by the library does links automatically if your resource supports them) – aromero Dec 31 '17 at 19:42
6

2020 update: active_model_serializer now supports this out of the box if you use json_api schema, but the docs also teach you how to add it if you use the json schema.

The docs are here: https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/howto/add_pagination_links.md

Below I explain how to achieve the desired results if you are using the json_api or the json adapters. Check which one you're using on ActiveModelSerializers.config.adapter.

If you are using the JSON API adapter (your ActiveModelSerializers.config.adapter = :json_api)

Pagination links will be included in your response automatically as long as the resource is paginated and if you are using the JsonApi adapter.

If you want pagination links in your response, use Kaminari or WillPaginate.

Kaminari examples
#array
@posts = Kaminari.paginate_array([1, 2, 3]).page(3).per(1)
render json: @posts

#active_record
@posts = Post.page(3).per(1)
render json: @posts
WillPaginate examples
#array
@posts = [1,2,3].paginate(page: 3, per_page: 1)
render json: @posts

#active_record
@posts = Post.page(3).per_page(1)
render json: @posts
ActiveModelSerializers.config.adapter = :json_api

ex:

{
  "data": [
    {
      "type": "articles",
      "id": "3",
      "attributes": {
        "title": "JSON API paints my bikeshed!",
        "body": "The shortest article. Ever.",
        "created": "2015-05-22T14:56:29.000Z",
        "updated": "2015-05-22T14:56:28.000Z"
      }
    }
  ],
  "links": {
    "self": "http://example.com/articles?page[number]=3&page[size]=1",
    "first": "http://example.com/articles?page[number]=1&page[size]=1",
    "prev": "http://example.com/articles?page[number]=2&page[size]=1",
    "next": "http://example.com/articles?page[number]=4&page[size]=1",
    "last": "http://example.com/articles?page[number]=13&page[size]=1"
  }
}

ActiveModelSerializers pagination relies on a paginated collection with the methods current_page, total_pages, and size, such as are supported by both Kaminari or WillPaginate.

If you are using the JSON adapter (your ActiveModelSerializers.config.adapter = :json)

If you are not using JSON adapter, pagination links will not be included automatically, but it is possible to do so using meta key.

Add this method to your base API controller.

def pagination_dict(collection)
  {
    current_page: collection.current_page,
    next_page: collection.next_page,
    prev_page: collection.prev_page, # use collection.previous_page when using will_paginate
    total_pages: collection.total_pages,
    total_count: collection.total_count
  }
end

Then, use it on your render method.

render json: posts, meta: pagination_dict(posts)

ex.

{
  "posts": [
    {
      "id": 2,
      "title": "JSON API paints my bikeshed!",
      "body": "The shortest article. Ever."
    }
  ],
  "meta": {
    "current_page": 3,
    "next_page": 4,
    "prev_page": 2,
    "total_pages": 10,
    "total_count": 10
  }
}

You can also achieve the same result if you have a helper method that adds the pagination info in the meta tag. For instance, in your action specify a custom serializer.

render json: @posts, each_serializer: PostPreviewSerializer, meta: meta_attributes(@posts)
#expects pagination!
def meta_attributes(collection, extra_meta = {})
  {
    current_page: collection.current_page,
    next_page: collection.next_page,
    prev_page: collection.prev_page, # use collection.previous_page when using will_paginate
    total_pages: collection.total_pages,
    total_count: collection.total_count
  }.merge(extra_meta)
end

Attributes adapter

This adapter does not allow us to use meta key, due to that it is not possible to add pagination links.

sandre89
  • 5,218
  • 2
  • 43
  • 64
  • The key here is `ActiveModelSerializers.config.adapter = :json_api` in a file in your config/intializers/ams.rb. – Gregory Ray Aug 27 '20 at 03:08
1

https://github.com/x1wins/tutorial-rails-rest-api/blob/master/lib/pagination.rb

# /lib/pagination.rb
class Pagination
  def self.build_json object, param_page = {}
    ob_name = object.name.downcase.pluralize
    json = Hash.new
    json[ob_name] = ActiveModelSerializers::SerializableResource.new(object.to_a, param_page: param_page)
    json[:pagination] = {
        current_page: object.current_page,
        next_page: object.next_page,
        prev_page: object.prev_page,
        total_pages: object.total_pages,
        total_count: object.total_count
    }
    return json
  end
end

how to use

#app/controller/posts_controller.rb
#post#index
render json: Pagination.build_json(@posts)

full source https://github.com/x1wins/tutorial-rails-rest-api

Changwoo Rhee
  • 179
  • 4
  • 11