Rails active_model_serializer with pagination
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
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.
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.