Plucker Serializer
About
Plucker serializer is a fast JSON serializer for ActiveRecord and Ruby objects. It is inspired by ActiveModelSerializers and Panko, and brings performance enhancements for real world Rails APIs.
The interface is very close to ActiveModelSerializers and Panko.
Plucker was created with performance in mind, and is most suitable for Rails APIs.
It uses different methods to achieve high throughput and low memory consumption :
- Plucker uses ActiveRecord's
pluck
function when possible to avoid going through ActiveRecord instantiation and to optimize database queries. - Serializer metadata is computed ahead of time, when serializer classes are parsed. Serializers have a
_descriptor
class attribute which contains all the information necessary to compute the serialization. - Plucker offers built-in caching for single objects, collections and associations. Caching associations and collections can result in great performance enhancements compared to simple single object caching.
Installation
To install Plucker, add this line to your application's Gemfile:
gem 'plucker_serializer'
And then execute:
$ bundle install
Getting Started
To create a serializer, create a class that inherits from Plucker::Base
:
class PostSerializer < Plucker::Base
attributes :title
end
class UserSerializer < Plucker::Base
attributes :id, :name, :age
has_many :posts, serializer: PostSerializer
end
To get the hash or JSON output for an object, instantiate your serializer class with an object and call to_hash
, to_h
, or to_json
:
post = Post.last
post_serialized = PostSerializer.new(post).to_json
{ "title": "my-post-title" }
and for a collection of objects, use the Plucker::Collection
class :
posts = Post.all
posts_serialized = Plucker::Collection.new(posts).to_json
[{ "title": "my-post-title" }, { "title": "my-second-post-title" }]
Describing your objects with Plucker
Attributes
A serializer can define attributes with the attribute
and attributes
functions.
class PostSerializer < Plucker::Base
attributes :title, :created_at
attribute :description
end
Attributes must be attributes of the serialized object, or they can come from a serializer method or block.
class PostSerializer < Plucker::Base
attribute :method_attribute
attribute :block_attribute do |object|
object.title.capitalize
end
def method_attribute
# object represents the serialized object and is available in the method context
object.title.parameterize
end
end
A key option can be passed to an attribute to define a different key in the serialized output :
class PostSerializer < Plucker::Base
attributes :title
attribute :description, key: :summary
end
{ "title": "my-post-title", "summary": "my-post-description" }
Associations
A serializer can define belongs_to
, has_one
and has_many
associations to fetch associated objects.
class PostSerializer < Plucker::Base
attributes :title, :description
has_one :author # AuthorSerializer will be used
belongs_to :category, key: :section # CategorySerializer will be used
has_many :tags, serializer: TagCustomSerializer
end
class AuthorSerializer < Plucker::Base
attributes :first_name, :last_name
end
class CategorySerializer < Plucker::Base
attributes :id, :display_name
end
class TagCustomSerializer < Plucker::Base
attributes :id, :display_name
end
By default, Plucker will use the ${MODEL}Serializer
serializer class for associations, but you can pass a custom serializer
with the serializer
option. Plucker will use the name of the model and not the name of the association, so for example if you
have an author
association that links a Post to a User, Plucker will use the UserSerializer
class by default.
As for attributes, it is also possible to pass a custom key for an association, and to add a block :
class PostSerializer < Plucker::Base
attributes :title, :description
has_many :tags, serializer: TagCustomSerializer do |object|
# block must return an ActiveRecord::Collection
object.tags.limit(1)
end
end
Model and serializer classes
Plucker must know the model name that is represented by the serializer.
By default, it will use the beginning of your serializer class name.
If your class is called PostSerializer
, the model used will be Post
. If your class is called PostTagSerializer
, the model used will be PostTag
.
If you class name is different from the model name, use the model
option to tell Plucker which model to use :
class PostCustomSerializer < Plucker::Base
model Post
attributes :title
end
For collections, as for associations, Plucker will use the serialized object class to compute the serializer class, but you can pass a custom serializer :
posts = Post.all
# Plucker will use the PostSerializer class by default
posts_serialized = Plucker::Collection.new(posts).to_json
# Use the serializer option to pass a custom serializer
posts_serialized_custom = Plucker::Collection.new(posts, serializer: CustomPostSerializer).to_json
Caching
Plucker has built-in caching for single objects, collections and associations.
To enable caching, create an initializer plucker.rb
and define a cache store :
Plucker.configure do |config|
config.cache_store = Rails.cache
end
Then enable caching in each serializer with the cache
option:
class PostCustomSerializer < Plucker::Base
cache
attributes :title
end
Plucker uses ActiveRecord's cache_key
and cache_version
for single objects and collections.
When caching a collection, ActiveRecord will query the database to get the last updated object. The cache key
will include a hash of the SQL query.
This can be a performance issue in some cases, depending on your application.
To avoid collection caching, use the cache
option with multi
when instantiating the collection :
posts = Post.all
posts_serialized = Plucker::Collection.new(posts, cache: :multi)
This will tell Plucker to not cache the whole collection but to cache each object separately.
Tests
Tests are written with RSpec. To run tests, run command :
$ rspec
Benchmarks
Benchmarks are available in the benchmarks
folder, to compare Plucker with ActiveModelSerializers and Panko.
To run benchmarks, use the rake command :
$ rake benchmarks
Plucker
Benchmark | ip/s | allocs/retained |
---|---|---|
Plucker_Simple_Posts_50 | 1,855.78 | 867/5 |
Plucker_Simple_Posts_1000 | 121.4 | 16199/126 |
Plucker_HasOne_Posts_50 | 87.53 | 10424/83 |
Plucker_HasOne_Posts_1000 | 21.83 | 78779/75 |
Plucker_HasMany_Posts_50 | 45.47 | 25023/72 |
Plucker_HasMany_Posts_1000 | 2.27 | 497243/2 |
Panko
Benchmark | ip/s | allocs/retained |
---|---|---|
Panko_ActiveRecord_Simple_Posts_50 | 2,306.29 | 721/24 |
Panko_ActiveRecord_Simple_Posts_1000 | 39.01 | 30799/81 |
Panko_ActiveRecord_HasOne_Posts_50 | 92.5 | 8187/80 |
Panko_ActiveRecord_HasOne_Posts_1000 | 39.19 | 31262/22026 |
Panko_ActiveRecord_HasMany_Posts_50 | 55.52 | 16893/102 |
Panko_ActiveRecord_HasMany_Posts_1000 | 2.63 | 331262/168026 |
ActiveModelSerializers 0.10
Benchmark | ip/s | allocs/retained |
---|---|---|
AMS_Simple_Posts_50 | 291.05 | 4625/162 |
AMS_Simple_Posts_1000 | 11.84 | 88838/77 |
AMS_HasOne_Posts_50 | 56.93 | 14456/87 |
AMS_HasOne_Posts_1000 | 6.02 | 155293/2 |
AMS_HasMany_Posts_50 | 20.26 | 45199/109 |
AMS_HasMany_Posts_1000 | 1.03 | 895293/2 |
Plucker shows significant performance improvements compared to ActiveModelSerializer.
When comparing to Panko, we can also see improvements in throughput and memory in some cases, thanks to the plucking strategy. Plucker is especially good in the case of objects with only attributes.
Panko seems to have memory problems for large collections.
The benchmarks do not use caching, and Plucker would see a huge boost compared to Panko if caching were enabled. We did not add it to the benchmarks because the comparison would not make sense as Panko does not enable caching.
License
The gem is available as open source under the terms of the MIT License.