ActiveModel::Serializers
encapsulates the JSON serialization of objects.
Objects that respond to read_attribute_for_serialization
(including ActiveModel
and ActiveRecord
objects) are supported.
Serializers know about both a model and the current_user
, so you can
customize serialization based upon whether a user is authorized to see the
content.
In short, serializers replace hash-driven development with object-oriented development.
The easiest way to install ActiveModel::Serializers
is to add it to your
Gemfile
:
gem "active_model_serializers"
Then, install it on the command line:
$ bundle install
If you must use a ruby 1.8 version (MRI 1.8.7, REE, Rubinius 1.8, or JRuby 1.8), you need to use version 0.8.x. Versions after 0.9.0 do not support ruby 1.8. To specify version 0.8, include this in your Gemfile:
gem "active_model_serializers", "~> 0.8.0"
The easiest way to create a new serializer is to generate a new resource, which will generate a serializer at the same time:
$ rails g resource post title:string body:string
This will generate a serializer in app/serializers/post_serializer.rb
for
your new model. You can also generate a serializer for an existing model with
the serializer generator:
$ rails g serializer post
The PORO should include ActiveModel::SerializerSupport. That's all you need to do to have your POROs supported.
For Rails versions before Rails 4 ActiveModel::Serializers expects objects to
implement read_attribute_for_serialization
.
In your controllers, when you use render :json
, Rails will now first search
for a serializer for the object and use it if available.
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
render json: @post
end
end
In this case, Rails will look for a serializer named PostSerializer
, and if
it exists, use it to serialize the Post
.
This also works with respond_with
, which uses to_json
under the hood. Also
note that any options passed to render :json
will be passed to your
serializer and available as @serialization_options
inside.
To specify a custom serializer for an object, you can specify the serializer when you render the object:
render json: @post, serializer: FancyPostSerializer
When controller does not inherit from ActionController::Base, include Serialization module manually:
class ApplicationController < ActionController::API
include ActionController::Serialization
end
In your controllers, when you use render :json
for an array of objects, AMS will
use ActiveModel::ArraySerializer
(included in this project) as the base serializer,
and the individual Serializer
for the objects contained in that array.
class PostSerializer < ActiveModel::Serializer
attributes :title, :body
end
class PostsController < ApplicationController
def index
@posts = Post.all
render json: @posts
end
end
Given the example above, the index action will return
{
"posts":
[
{ "title": "Post 1", "body": "Hello!" },
{ "title": "Post 2", "body": "Goodbye!" }
]
}
By default, the root element is the name of the controller. For example, PostsController
generates a root element "posts". To change it:
render json: @posts, root: "some_posts"
You may disable the root element for arrays at the top level, which will result in more concise json. See the next section for ways on how to do this. Disabling the root element of the array with any of those methods will produce
[
{ "title": "Post 1", "body": "Hello!" },
{ "title": "Post 2", "body": "Goodbye!" }
]
To specify a custom serializer for the items within an array:
render json: @posts, each_serializer: FancyPostSerializer
By default the setting of serializer is in controller as described above which is the recommended way. However, there may be cases you need to render the json object elsewhere say in a helper or a view when controller is only for main object.
Then you can render the serialized JSON independently.
def current_user_as_json_helper
CurrentUserSerializer.new(current_user).to_json
end
You can also render an array of objects using ArraySerializer.
def users_array_as_json_helper(users)
ActiveModel::ArraySerializer.new(users, each_serializer: UserSerializer).to_json
end
You have 4 options to disable the root element, each with a slightly different scope:
In an initializer:
# Disable for all serializers (except ArraySerializer)
ActiveModel::Serializer.root = false
# Disable for ArraySerializer
ActiveModel::ArraySerializer.root = false
render json: @posts, root: false
class CustomArraySerializer < ActiveModel::ArraySerializer
self.root = false
end
# controller:
render json: @posts, serializer: CustomArraySerializer
If you define default_serializer_options
method in your controller,
all serializers in actions of this controller and it's children will use them.
One of the options may be root: false
def default_serializer_options
{
root: false
}
end
You can specify that serializers use the lower-camel key format at the config, class or instance level.
ActiveModel::Serializer.setup do |config|
config.key_format = :lower_camel
end
class BlogLowerCamelSerializer < ActiveModel::Serializer
format_keys :lower_camel
end
BlogSerializer.new(object, key_format: :lower_camel)
You can specify that serializers use unsuffixed names as association keys by default.
ActiveModel::Serializer.setup do |config|
config.default_key_type = :name
end
This will build association keys like comments
or author
instead of comment_ids
or author_id
.
If you find that your project is already relying on the old rails to_json
change render :json
to render json: @your_object.to_json
.
Once you have a serializer, you can specify which attributes and associations you would like to include in the serialized form.
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :comments
end
For specified attributes, a serializer will look up the attribute on the
object you passed to render :json
. It uses
read_attribute_for_serialization
, which ActiveRecord
objects implement as a
regular attribute lookup.
Before looking up the attribute on the object, a serializer will check for the presence of a method with the name of the attribute. This allows serializers to include properties beyond the simple attributes of the model. For example:
class PersonSerializer < ActiveModel::Serializer
attributes :first_name, :last_name, :full_name
def full_name
"#{object.first_name} #{object.last_name}"
end
end
Within a serializer's methods, you can access the object being
serialized as object
.
Since this shadows any attribute named object
, you can include them through object.object
. For example:
class VersionSerializer < ActiveModel::Serializer
attributes :version_object
def version_object
object.object
end
end
You can also access the scope
method, which provides an
authorization context to your serializer. By default, the context
is the current user of your application, but this
can be customized.
Serializers provide a method named filter
, which should return an array
used to determine what attributes and associations should be included in the output.
This is typically used to customize output based on current_user
. For example:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :author
def filter(keys)
if scope.admin?
keys
else
keys - [:author]
end
end
end
And it's also safe to mutate keys argument by doing keys.delete(:author) in case you want to avoid creating two extra arrays. Note that if you do an in-place modification, you still need to return the modified array.
If you would like the key in the outputted JSON to be different from its name in ActiveRecord, you can declare the attribute with the different name and redefine that method:
class PostSerializer < ActiveModel::Serializer
# look up subject on the model, but use title in the JSON
def title
object.subject
end
attributes :id, :body, :title
has_many :comments
end
If you would like to add meta information to the outputted JSON, use the :meta
option:
render json: @posts, serializer: CustomArraySerializer, meta: {total: 10}
The above usage of :meta
will produce the following:
{
"meta": { "total": 10 },
"posts": [
{ "title": "Post 1", "body": "Hello!" },
{ "title": "Post 2", "body": "Goodbye!" }
]
}
If you would like to change the meta key name you can use the :meta_key
option:
render json: @posts, serializer: CustomArraySerializer, meta_object: { total: 10 }, meta_key: :meta_object
The above usage of :meta_key
will produce the following:
{
"meta_object": { "total": 10 },
"posts": [
{ "title": "Post 1", "body": "Hello!" },
{ "title": "Post 2", "body": "Goodbye!" }
]
}
When using meta information, your serializer cannot have the { root: false }
option, as this would lead to
invalid JSON. If you do not have a root key, the meta information will be ignored.
If you would like direct, low-level control of attribute serialization, you can
completely override the attributes
method to return the hash you need:
class PersonSerializer < ActiveModel::Serializer
attributes :first_name, :last_name
def attributes
hash = super
if scope.admin?
hash["ssn"] = object.ssn
hash["secret"] = object.mothers_maiden_name
end
hash
end
end
For specified associations, the serializer will look up the association and
then serialize each element of the association. For instance, a has_many :comments
association will create a new CommentSerializer
for each comment
and use it to serialize the comment.
By default, serializers simply look up the association on the original object. You can customize this behavior by implementing a method with the name of the association and returning a different Array. Often, you will do this to customize the objects returned based on the current user (scope).
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :comments
# only let the user see comments he created.
def comments
object.comments.where(created_by: scope)
end
end
As with attributes, you can change the JSON key that the serializer should use for a particular association.
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
# look up comments, but use +my_comments+ as the key in JSON
has_many :comments, root: :my_comments
end
Also, as with attributes, serializers will execute a filter method to determine which associations should be included in the output. For example:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :comments
def filter(keys)
keys.delete :comments if object.comments_disabled?
keys
end
end
Or ...
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_one :author
has_many :comments
def filter(keys)
keys.delete :author unless scope.admin?
keys.delete :comments if object.comments_disabled?
keys
end
end
You may also use the :serializer
option to specify a custom serializer class and the :polymorphic
option to specify an association that is polymorphic (STI), e.g.:
has_many :comments, serializer: CommentShortSerializer
has_one :reviewer, polymorphic: true
Serializers are only concerned with multiplicity, and not ownership. belongs_to
ActiveRecord associations can be included using has_one
in your serializer.
By default, associations will be embedded inside the serialized object. So if you have a post, the outputted JSON will look like:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comments": [
{ "id": 1, "body": "what a dumb post" }
]
}
}
This is convenient for simple use-cases, but for more complex clients, it is better to supply an Array of IDs for the association. This makes your API more flexible from a performance standpoint and avoids wasteful duplication.
To embed IDs instead of associations, simply use the embed
class method:
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title, :body
has_many :comments
end
Now, any associations will be supplied as an Array of IDs:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ 1, 2, 3 ]
}
}
You may also choose to embed the IDs by the association's name underneath a
key
for the resource. For example, say we want to change comment_ids
to comments
underneath a links
key:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :comments, embed: :ids, key: :comments, embed_namespace: :links
end
The JSON will look like this:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"links": {
"comments": [ 1, 2, 3 ]
}
}
}
Alternatively, you can choose to embed only the ids or the associated objects per association:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :comments, embed: :objects
has_many :tags, embed: :ids
end
The JSON will look like this:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comments": [
{ "id": 1, "body": "what a dumb post" }
],
"tag_ids": [ 1, 2, 3 ]
}
}
In addition to supplying an Array of IDs, you may want to side-load the data alongside the main object. This makes it easier to process the entire package of data without having to recursively scan the tree looking for embedded information. It also ensures that associations that are shared between several objects (like tags), are only delivered once for the entire payload.
You can specify that the data be included like this:
class PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :title, :body
has_many :comments
end
Assuming that the comments also has_many :tags
, you will get a JSON like
this:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ 1, 2 ]
},
"comments": [
{ "id": 1, "body": "what a dumb post", "tag_ids": [ 1, 2 ] },
{ "id": 2, "body": "i liked it", "tag_ids": [ 1, 3 ] },
],
"tags": [
{ "id": 1, "name": "short" },
{ "id": 2, "name": "whiny" },
{ "id": 3, "name": "happy" }
]
}
If you would like to namespace association JSON underneath a certain key in
the root document (say, linked
), you can specify an embed_in_root_key
:
class PostSerializer < ActiveModel::Serializer
embed :ids, include: true, embed_in_root_key: :linked
attributes: :id, :title, :body
has_many :comments, :tags
end
The above would yield the following JSON document:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ 1, 2 ]
},
"linked": {
"comments": [
{ "id": 1, "body": "what a dumb post", "tag_ids": [ 1, 2 ] },
{ "id": 2, "body": "i liked it", "tag_ids": [ 1, 3 ] },
],
"tags": [
{ "id": 1, "name": "short" },
{ "id": 2, "name": "whiny" },
{ "id": 3, "name": "happy" }
]
}
}
When side-loading data, your serializer cannot have the { root: false }
option,
as this would lead to invalid JSON. If you do not have a root key, the include
instruction will be ignored
You can also specify a different root for the embedded objects than the key used to reference them:
class PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :title, :body
has_many :comments, key: :comment_ids, root: :comment_objects
end
This would generate JSON that would look like this:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ 1 ]
},
"comment_objects": [
{ "id": 1, "body": "what a dumb post" }
]
}
You can also specify a different attribute to use rather than the ID of the objects:
class PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :title, :body
has_many :comments, key: :external_id
end
This would generate JSON that would look like this:
{
"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ "COMM001" ]
},
"comments": [
{ "id": 1, "external_id": "COMM001", "body": "what a dumb post" }
]
}
NOTE: The embed :ids
mechanism is primary useful for clients that process
data in bulk and load it into a local store. For these clients, the ability to
easily see all of the data per type, rather than having to recursively scan the
data looking for information, is extremely useful.
If you are mostly working with the data in simple scenarios and manually making Ajax requests, you probably just want to use the default embedded behavior.
Because we need both the id and the type to be able to identify a polymorphic associated model, these are serialized in a slightly different format than common ones.
When embedding entire objects:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title
has_many :attachments, polymorphic: true
end
{
"post": {
"id": 1,
"title": "New post",
"attachments": [
{
"type": "image",
"image": {
"id": 3,
"name": "logo",
"url": "http://images.com/logo.jpg"
}
},
{
"type": "video",
"video": {
"id": 12,
"uid": "XCSSMDFWW",
"source": "youtube"
}
}
]
}
}
When embedding ids:
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title
has_many :attachments, polymorphic: true
end
{
"post": {
"id": 1,
"title": "New post",
"attachment_ids": [
{
"type": "image",
"id": 12
},
{
"type": "video",
"id": 3
}
]
}
}
In a serializer, current_user
is the current authorization scope which the controller
provides to the serializer when you call render :json
. By default, this is
current_user
, but can be customized in your controller by calling
serialization_scope
:
class ApplicationController < ActionController::Base
serialization_scope :current_admin
end
The above example will also change the scope from current_user
to
current_admin
.
Please note that, until now, serialization_scope
doesn't accept a second
object with options for specifying which actions should or should not take a
given scope in consideration.
To be clear, it's not possible, yet, to do something like this:
class SomeController < ApplicationController
serialization_scope :current_admin, except: [:index, :show]
end
So, in order to have a fine grained control of what each action should take in consideration for its scope, you may use something like this:
class CitiesController < ApplicationController
serialization_scope nil
def index
@cities = City.all
render json: @cities, each_serializer: CitySerializer
end
def show
@city = City.find(params[:id])
render json: @city, scope: current_admin
end
end
Assuming that the current_admin
method needs to make a query in the database
for the current user, the advantage of this approach is that, by setting
serialization_scope
to nil
, the index
action no longer will need to make
that query, only the show
action will.
In order to test a Serializer, you can just call .new
on it, passing the object to serialize:
class TestPostSerializer < Minitest::Test
def setup
@serializer = PostSerializer.new Post.new(id: 123, title: 'some title', body: 'some text')
end
def test_special_json_for_api
assert_equal '{"post":{"id":123,"title":"some title","body":"some text"}}', @serializer.to_json
end
describe PostSerializer do
it "creates special JSON for the API" do
serializer = PostSerializer.new Post.new(id: 123, title: 'some title', body: 'some text')
expect(serializer.to_json).to eql('{"post":{"id":123,"title":"some title","body":"some text"}}')
end
end
NOTE: This functionality was removed from AMS and it's in the TODO list. We need to re-think and re-design the caching strategy for the next version of AMS.
To cache a serializer, call cached
and define a cache_key
method:
class PostSerializer < ActiveModel::Serializer
cached # enables caching for this serializer
attributes :title, :body
def cache_key
[object, scope]
end
end
The caching interface uses Rails.cache
under the hood.
By default, new serializers descend from ActiveModel::Serializer. However, if you wish to share behaviour across your serializers you can create an ApplicationSerializer at app/serializers/application_serializer.rb
:
class ApplicationSerializer < ActiveModel::Serializer
end
Any newly generated serializers will automatically descend from ApplicationSerializer.
$ rails g serializer post
now generates:
class PostSerializer < ApplicationSerializer
attributes :id
end
ActiveModel::Serializers
is capable of producing complex JSON views/large object
trees, and it may be tempting to design in this way so that your client can make
fewer requests to get data and so that related querying can be optimized.
However, keeping things simple in your serializers and controllers may
significantly reduce complexity and maintenance over the long-term development
of your application. Please consider reducing the complexity of the JSON views
you provide via the serializers as you build out your application, so that
controllers/services can be more easily reused without a lot of complexity
later.
As you develop your controllers or other code that utilizes serializers, try to avoid n+1 queries by ensuring that data loads in an optimal fashion, e.g. if you are using ActiveRecord, you might want to use query includes or joins as needed to make the data available that the serializer(s) need.