Barley is a fast and efficient ActiveModel serializer.
Cerealize your ActiveModel objects into flat hashes with a dead simple, yet versatile DSL, and caching and type-checking baked in. Our daily bread is to make your API faster.
You don't believe us? Check out the benchmarks. 😎
API documentation
Check out the API documentation here.
Usage
Add the Barley::Serializable
module to your ActiveModel object.
# /app/models/user.rb
class User < ApplicationRecord
include Barley::Serializable
end
Then define your attributes and associations in a serializer class.
# /app/serializers/user_serializer.rb
class UserSerializer < Barley::Serializer
attributes id: Types::Strict::Integer, :name
attribute :email
attribute :value, type: Types::Coercible::Integer
many :posts
many :posts, key_name: :featured, scope: :featured
many :posts, key_name: :popular, scope: -> { where("views > 10_000").limit(3) }
many :posts, key_name: :in_current_language, scope: -> (context) { where(language: context.language) }
one :group, serializer: CustomGroupSerializer
many :related_users, key: :friends, cache: true
one :profile, cache: { expires_in: 1.day } do
attributes :avatar, :social_url
attribute :badges do
object.badges.map(&:display_name)
end
end
end
Then just use the as_json
method on your model.
user = User.find(1)
user.as_json(only: [:id, :name, posts: [:id, :title]])
Installation
Add this line to your application's Gemfile:
gem "barley"
And then execute:
$ bundle
Or install it yourself as:
$ gem install barley
Defining the serializer
Barley uses the model name to find the serializer class. For example, if you have a User
model, Barley will look for a UserSerializer
class.
You can also define the serializer class with the serializer
macro.
# /app/models/user.rb
class User < ApplicationRecord
include Barley::Serializable
serializer UserSerializer
end
DSL
Attributes
You can define attributes with the attributes
macro.
attributes :id, :name, :email, :created_at, :updated_at
You can also define attributes one by one, or a mix of both.
attributes :id, :name, :email
attribute :created_at
attribute :updated_at
You can also define a custom attribute with a block. You will have a object
variable available in the block. It is the object you are serializing.
attribute :full_name do
"#{object.first_name} #{object.last_name}"
end
You can also set a custom key name for the attribute with the key_name
option.
attribute :updated_at, key: :last_change
Associations
One-to-one
You can define a one-to-one association with the one
macro.
one :group
Custom serializer and caching
You can define a custom serializer for the association with the serializer
option, and / or caching options with the cache
option.
one :group, serializer: CustomGroupSerializer, cache: { expires_in: 1.hour }
You can of course define serializers with inner classes for simple needs.
class UserSerializer < Barley::Serializer
attributes :id, :name, :email, :created_at, :updated_at
one :group, serializer: LocalGroupSerializer
class LocalGroupSerializer < Barley::Serializer
attributes :id, :name
end
end
Key name
You can also pass a key name for the association with the key_name
option.
one :group, key_name: :team
One-to-many
You can define a one-to-many association with the many
macro.
many :posts
Custom serializer and caching
You can define a custom serializer for the association with the serializer
option, and / or caching options with the cache
option.
many :posts, serializer: CustomPostSerializer, cache: { expires_in: 1.hour }
Scope
You can pass a scope to the association with the scope
option. It can either be a symbol referencing a named scope on your associated model, or a lambda.
many :posts, scope: :published # given you have a scope named `published` on your Post model
many :posts, scope: -> { where(published: true).limit(4) }
You can also pass a context to the lambda. See the context section for more details.
many :posts, scope: -> (context) { where(language: context.language) }
Key name
You can also pass a key name for the association with the key_name
option.
many :posts, key_name: :articles
Associations with blocks
Feel like using a block to define your associations? You can do that too.
one :group do
attributes :id, :name
end
many :posts do
attributes :id, :title, :body
one :author do
attributes :name, :email
end
end
Of course, all the options available for the one
and many
macros are also available for the block syntax.
many :posts, key_name: :featured do
attributes :id, :title, :body
end
Context
You can pass a context to the serializer with the with_context
method.
serializer = PostSerializer.new(Post.last).with_context(current_user: current_user)
This context will be available in the serializer with the context
method. It is also available in nested serializers.
class PostSerializer < Barley::Serializer
attributes :id, :title, :body
attribute :is_owner do
object.user == context.current_user
end
many :comments do
many :likes do
attribute :is_owner do
object.user == context.current_user # context is here too!
end
end
end
end
The context is also available in the scope of the lambda passed to the scope
option of the many
macro. See the scope section for more details.
many :posts, scope: -> (context) { where(language: context.language) }
Using a custom context object
Barley generates a Struct from the context hash you pass to the with_context method. But you can also pass a custom context object directly in the initializer instead.
my_context = Struct.new(:current_user).new(current_user)
serializer = PostSerializer.new(Post.last, context: my_context)
Generators
You have two generators available. One to generate the serializer class:
rails generate barley:serializer User
# or
rails generate barley:serializer User --name=CustomUserSerializer
And one to generate both the serializer class and add the module to the model:
rails generate barley:serializable User
# or
rails generate barley:serializable User --name=CustomUserSerializer
Serialization options
You can pass a hash of options to the as_json
method.
user = User.find(1)
user.as_json(serializer: CustomUserSerializer, cache: { expires_in: 1.hour })
Beware, this gem overrides the as_json
method on your model. Calling as_json
with include
, only
, or except
options will not work.
Why? We believe it defeats the purpose of this gem. If you want to customize the serialization of your model, you should use a serializer class.
Caching
Barley supports caching out of the box. Just pass cache: true
to the serializer
macro.
# /app/models/user.rb
# ...
serializer UserSerializer, cache: true
Or you can pass a hash of options to the serializer
macro.
# /app/models/user.rb
# ...
serializer UserSerializer, cache: { expires_in: 1.hour }
Caching options
Barley uses the MemoryStore by default. You can change the cache store with the cache_store
option in an initializer.
# /config/initializers/barley.rb
Barley.configure do |config|
config.cache_store = ActiveSupport::Cache::RedisCacheStore.new
end
Type checking
Barley can check the type of the object you are serializing with the dry-types gem.
It will raise an error if the object is not of the expected type, or coerce it to the correct type and perform constraints checks.
module Types
include Dry.Types()
end
class UserSerializer < Barley::Serializer
attributes id: Types::Strict::Integer, name: Types::Strict::String, email: Types::Strict::String.constrained(format: URI::MailTo::EMAIL_REGEXP)
attribute :role, type: Types::Coercible::String do
object.role.integer_or_string_coercible_value
end
end
Check out dry-types for all options and available types.
Breakfast mode 🤡 (coming soon)
You will soon be able to replace all occurrences of Serializer
with Cerealizer
in your codebase. Just for fun. And for free.
# /app/models/user.rb
class User < ApplicationRecord
include Barley::Cerealizable
cerealizer UserCerealizer
end
# app/cerealizers/user_cerealizer.rb
class UserCerealizer < Barley::Cerealizer
attributes :id, :name, :email, :created_at, :updated_at
many :posts
one :group
end
rails generate barley:cerealizer User
# etc.
Ah ah ah. This is so funny.
Note: we are thinking about adding a Surrealizer
class for the most advanced users. Stay tuned.
JSON:API
Barley does not serialize to the JSON:API standard. We prefer to keep it simple and fast.
Benchmarks
This gem is blazing fast and efficient. It is 2 to 3 times faster than ActiveModel::Serializer and twice as fast as FastJsonapi. Memory object allocation is also much lower.
With caching enabled, it is just mind-blowing. We think. Disclaimer: we do not serialize to the JSON:API standard, so that might be the reason why we are so fast.
This is the result we get with the benchmark script used in the AMS repo on an Apple Silicon M1Pro processor. Check it out for yourself here.
bundle exec ruby benchmark.rb
-- create_table("comments", {:force=>:cascade})
-> 0.0128s
-- create_table("posts", {:force=>:cascade})
-> 0.0002s
-- create_table("users", {:force=>:cascade})
-> 0.0002s
Warming up --------------------------------------
ams 3.000 i/100ms
jsonapi-rb 9.000 i/100ms
barley 9.000 i/100ms
barley-cache 460.000 i/100ms
ams eager 8.000 i/100ms
jsonapi-rb eager 33.000 i/100ms
barley eager 37.000 i/100ms
barley-cache eager 70.000 i/100ms
Calculating -------------------------------------
ams 67.770 (± 1.9%) i/s - 657.000 in 10.042270s
jsonapi-rb 157.239 (± 2.3%) i/s - 1.521k in 10.010257s
barley 96.909 (± 2.4%) i/s - 945.000 in 10.036316s
barley-cache 4.589k (± 2.8%) i/s - 44.620k in 10.010004s
ams eager 68.237 (± 6.2%) i/s - 592.000 in 10.087602s
jsonapi-rb eager 289.733 (± 3.7%) i/s - 2.805k in 10.033912s
barley eager 406.732 (± 3.2%) i/s - 3.922k in 10.020076s
barley-cache eager 639.935 (± 2.4%) i/s - 6.300k in 10.053710s
with 95.0% confidence
Comparison:
barley-cache : 4589.3 i/s
barley-cache eager: 639.9 i/s - 7.17x (± 0.27) slower
barley eager: 406.7 i/s - 11.29x (± 0.49) slower
jsonapi-rb eager: 289.7 i/s - 15.83x (± 0.74) slower
jsonapi-rb : 157.2 i/s - 29.17x (± 1.08) slower
barley : 96.9 i/s - 47.37x (± 1.75) slower
ams eager: 68.2 i/s - 67.25x (± 4.69) slower
ams : 67.8 i/s - 67.72x (± 2.31) slower
with 95.0% confidence
Calculating -------------------------------------
ams 1.300M memsize ( 246.338k retained)
16.838k objects ( 2.492k retained)
50.000 strings ( 50.000 retained)
jsonapi-rb 926.082k memsize ( 197.338k retained)
9.565k objects ( 1.776k retained)
50.000 strings ( 50.000 retained)
barley 1.102M memsize ( 177.490k retained)
12.930k objects ( 1.876k retained)
50.000 strings ( 35.000 retained)
barley-cache 46.090k memsize ( 1.308k retained)
502.000 objects ( 17.000 retained)
42.000 strings ( 6.000 retained)
ams eager 1.068M memsize ( 233.994k retained)
13.723k objects ( 2.308k retained)
50.000 strings ( 50.000 retained)
jsonapi-rb eager 694.430k memsize ( 194.826k retained)
6.450k objects ( 1.651k retained)
50.000 strings ( 50.000 retained)
barley eager 354.326k memsize ( 124.282k retained)
3.694k objects ( 1.208k retained)
50.000 strings ( 32.000 retained)
barley-cache eager 216.790k memsize ( 107.228k retained)
2.376k objects ( 956.000 retained)
50.000 strings ( 33.000 retained)
Comparison:
barley-cache : 46090 allocated
barley-cache eager: 216790 allocated - 4.70x more
barley eager: 354326 allocated - 7.69x more
jsonapi-rb eager: 694430 allocated - 15.07x more
jsonapi-rb : 926082 allocated - 20.09x more
ams eager: 1068038 allocated - 23.17x more
barley : 1102354 allocated - 23.92x more
ams : 1299674 allocated - 28.20x more
License
The gem is available as open source under the terms of the MIT License.
Contributing
You can contribute in several ways: reporting bugs, suggesting features, or contributing code. See our contributing guidelines
Make sure you adhere to our code of conduct. We aim to keep this project open and inclusive.
Security
Please refer to our security guidelines