AdequateSerialization
AdequateSerialization
allows you to define serializers that will convert your objects into simple hashes that are suitable for variable purposes such as caching or using in an HTTP response. It stems from the simple idea of giving slightly more control over the as_json
method that gets called when objects are serialized using Rails' default controller serialization.
- Installation
- Usage
- Defining attributes
- :if
- :unless
- :optional
- Attaching objects
- Usage with Rails
- Cache busting
- Caching plain objects
- Advanced
- Defining attributes
- Development
- Contributing
- License
Installation
Add this line to your application's Gemfile:
gem 'adequate_serialization'
And then execute:
$ bundle
Or install it yourself as:
$ gem install adequate_serialization
Usage
There are two ways to define the serialization process for objects.
For larger objects where it makes sense to define the serialization in a separate class, you should include the AdequateSerialization::Serializable
in the object that you want to be able to serialize. Then, define a serializer matching the name of that object, postfixed with "Serializer"
, as in:
class UserSerializer < AdequateSerialization::Serializer
attribute :id, :name, :title
end
For smaller objects where it makes sense to define the serialization inline, you can include the result of the AdequateSerialization::inline
method, as in:
class User
include AdequateSerialization.inline { attribute :id, :name, :title }
...
end
For both types of serialization definition, you can then use the AdequateSerialization
DSL to define the attributes that are available to the serializer. You can then call as_json
on any instance of that object to get the resultant hash. Below is an example:
User.new(id: 1, name: 'Clark Kent', title: 'Superman').as_json
# => {:id=>1, :name=>"Clark Kent", :title=>"Superman"}
Defining attributes
The AdequateSerialization::Serializer
DSL is just the one attribute
method. You can pass as many names as you want, and each attribute will become a key in the resultant serialized hash. If you need to build a "synthesized" attribute (one that is defined in the serializer), you can do so with a block that receives the object as an argument, as in:
class UserSerializer < AdequateSerialization::Serializer
attribute :double_name do |user|
user.name * 2
end
end
There are also a couple of options that you can pass to the attribute
method as the last argument that modify the serializer's behavior, listed below.
:if
If you pass an :if
condition, that method will be called on the serializable object to determine whether or not that attribute should be included in the resultant hash, as in:
class UserSerializer < AdequateSerialization::Serializer
attribute :title, if: :manager?
end
user = User.new(...)
user.as_json
# => {:id=>1, :name=>"Clark Kent"}
user.update(manager: true)
user.as_json
# => {:id=>1, :name=>"Clark Kent", :title=>"Superman"}
:unless
This is the same as the :if
option, but will result in the opposite behavior (the attribute will be present if the predicate is not met).
:optional
There are times when you want to include an attribute that you normally wouldn't. For example, if you have both Post
and Comment
objects, normally you wouldn't include the post
attribute on the child comment
objects. However, if you're serializing just the comment, it might be useful to have the post
attached. In this case, you could mark the attribute as optional
and it would only be included if it was listed in the :includes
option passed to the as_json
method, as in:
class PostSerializer < AdequateSerialization::Serializer
attribute :id, :title, :body
attribute :comments, optional: true
end
class CommentSerializer < AdequateSerialization::Serializer
attribute :id, :body
attribute :post, optional: true
end
comment = Comment.new(...)
comment.as_json
# => {:id=>1, :body=>"This is a great gem!"}
comment.as_json(includes: :post)
# => {:id=>1, :body=>"This is a great gem!", :post=>{:id=>1, :title=>"Introducing Adequate Serializer", :body=>"This is adequate serializer."}}
The includes
key can take either a single name or an array of names.
Attaching objects
There are times where it's more performant to serialize the objects using normal serialization and to attach an additional attribute later. For instance, you could serialize all of the posts and then attach whether or not a user had upvoted them. In that case, there's a special syntax that looks like the below:
upvotes =
User.upvotes.each_with_object({}) do |post, votes|
votes[post.id] = true
end
# => {1=>true}
Post.all.map(&:as_json)
# => [{:id=>1}, {:id=>2}]
posts = Post.all.map { |post| post.as_json(attach: { upvoted: upvotes }) }
# => [{:id=>1, :upvoted=>true}, {:id=>2, :upvoted=>false}]
This relies on the objects to which you are attaching having an id
attribute and the attachable hash being an index of id
pointing to the attribute value.
Usage with Rails
If ::Rails
is defined when adequate_serialization
is required, it will hook into ActiveRecord
in three ways:
- By including
AdequateSerializer::Serializable
inActiveRecord::Base
so that all of your models will be serializable by overwritingActiveRecord::Base
'sas_json
method, which by default will useRails.cache.fetch
. - By overwriting
ActiveRecord::Relation
'sas_json
method to use theAdequateSerializer::Rails::RelationSerializer
object, which by default will use theRails.cache.fetch_multi
method in order to more efficiently serialize all of the records in the relation. - By introducing cache busting behavior in the background using
ActiveJob
if you're serializing objects outside of a one-to-many relationship.
Cache busting
When using adequate_serialization
with rails
, each attribute
call will check if you're serializing an association. If you are, then it will ensure you have appropriate caching behavior enabled:
- If it's a
has_many
orhas_one
association, then it will make sure that the inverse has thetouch: true
option on the association. - If it's a
belongs_to
association, then it will add anafter_update_commit
hook to the inverse class that will loop through the associated objects and bust the association using anActiveJob
task.
You can visualize this cache busting behavior with a prebaked Rack application that is shipped with this gem by adding the following to your config/routes.rb
file:
if Rails.env.development?
mount AdequateSerialization::Rails::CacheVisualization,
at: '/cache_visualization'
end
This will allow you to view which caches will bust which others in development by navigating to your application's /cache_visualization
path.
Caching plain objects
You can still use plain objects to be serialized, and if you want to take advantage of the caching behavior, you can define a cache_key
method on the objects that you're serializing. This will cause AdequateSerialization
to start putting them into the Rails cache.
The result is that you can now this in your controllers:
class UsersController
def show
user = User.find(params[:id])
render json: { user: user }
end
end
and the response will be the serialized user. You can pass additional options that will get forwarded on to the serializer as well, as in:
class UsersController
def show
user = User.find(params[:id])
render json: { user: user }, includes: :title
end
end
and the result will now contain the title
attribute (provided it was configured as an optional attribute). All options that previously were passed in to the as_json
method get forwarded appropriately.
Advanced
The serialization process happens through a series of AdequateSerialization::Steps
. The caching behavior mentioned in the Usage with Rails
section is one such step that gets introduced. You can introduce more yourself like so:
class LoggingStep < AdequateSerialization::Steps::Step
def apply(response)
Logger.log("#{response.object} is being serialized with #{response.opts} options")
apply_next(response)
end
end
AdequateSerialization.prepend(LoggingStep)
This will cause this object to be placed into the list of steps taken to serialize objects, and can be used for much more powerful and advanced workflows.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/CultureHQ/adequate_serialization.
License
The gem is available as open source under the terms of the MIT License.