This was the original design document for serializers. It is useful mostly for historical purposes as the public API has changed.h2. Rails SerializersThis guide describes how to use Active Model serializers to build non-trivial JSON services in Rails. By reading this guide, you will learn:* When to use the built-in Active Model serialization* When to use a custom serializer for your models* How to use serializers to encapsulate authorization concerns* How to create serializer templates to describe the application-wide structure of your serialized JSON* How to build resources not backed by a single database table for use with JSON servicesThis guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose aJSON API that may return different results based on the authorization status of the user.h3. SerializationBy default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additionalparameter to control which properties and associations Rails should include in the serialized output.When building a web application that uses JavaScript to retrieve JSON data from the server, this mechanism has historically been the primaryway that Rails developers prepared their responses. This works great for simple cases, as the logic for serializing an Active Record objectis neatly encapsulated in Active Record itself.However, this solution quickly falls apart in the face of serialization requirements based on authorization. For instance, a web servicemay choose to expose additional information about a resource only if the user is entitled to access it. In addition, a JavaScript front-endmay want information that is not neatly described in terms of serializing a single Active Record object, or in a different format than.In addition, neither the controller nor the model seems like the correct place for logic that describes how to serialize an model object*for the current user*.Serializers solve these problems by encapsulating serialization in an object designed for this purpose. If the default to_json semantics,with at most a few configuration options serve your needs, by all means continue to use the built-in to_json. If you find yourself doinghash-driven-development in your controllers, juggling authorization logic and other concerns, serializers are for you!h3. The Most Basic SerializerA basic serializer is a simple Ruby object named after the model class it is serializing.
class PostSerializer def initialize(post, scope) post, @scope = post, scope end def as_json { post: { title: @post.name, body: @post.body } } endend</pre>A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, theauthorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer alsoimplements an +as_json+ method, which returns a Hash that will be sent to the JSON encoder.Rails will transparently use your serializer when you use +render :json+ in your controller.<pre lang="ruby">class PostsController < ApplicationController def show @post = Post.find(params[:id]) render json: @post endend</pre>Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer whenyou use +respond_with+ as well.h4. +serializable_hash+In general, you will want to implement +serializable_hash+ and +as_json+ to allow serializers to embed associated contentdirectly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root.<pre lang="ruby">class PostSerializer def initialize(post, scope) @post, @scope = post, scope end def serializable_hash { title: @post.name, body: @post.body } end def as_json { post: serializable_hash } endend</pre>h4. AuthorizationLet's update our serializer to include the email address of the author of the post, but only if the current user has superuseraccess.<pre lang="ruby">class PostSerializer def initialize(post, scope) @post, @scope = post, scope end def as_json { post: serializable_hash } end def serializable_hash hash = post hash.merge!(super_data) if super? hash endprivate def post { title: @post.name, body: @post.body } end def super_data { email: @post.email } end def super? @scope.superuser? endend</pre>h4. TestingOne benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serializationlogic in isolation.<pre lang="ruby">require "ostruct"class PostSerializerTest < ActiveSupport::TestCase # For now, we use a very simple authorization structure. These tests will need # refactoring if we change that. plebe = OpenStruct.new(super?: false) god = OpenStruct.new(super?: true) post = OpenStruct.new(title: "Welcome to my blog!", body: "Blah blah blah", email: "tenderlovegmail.com") test “a regular user sees just the title and body” do json = PostSerializer.new(post, plebe).to_json hash = JSON.parse(json) assert_equal post.title, hash.delete(“title”) assert_equal post.body, hash.delete(“body”) assert_empty hash end test “a superuser sees the title, body and email” do json = PostSerializer.new(post, god).to_json hash = JSON.parse(json) assert_equal post.title, hash.delete(“title”) assert_equal post.body, hash.delete(“body”) assert_equal post.email, hash.delete(“email”) assert_empty hash endend
class PostSerializer # @param [~body, ~title, ~email] post the post to serialize # @param [~super] scope the authorization scope for this serializer def initialize(post, scope) @post, @scope = post, scope end # …end
class PostSerializer < ActiveModel::Serializer attributes :title, :body def serializable_hash hash = attributes hash.merge!(super_data) if super? hash endprivate def super_data { email: @post.email } end def super? @scope.superuser? endend
class PostSerializer < ActiveModel::Serializer attributes :title, :bodyprivate def attributes hash = super hash.merge!(email: post.email) if super? hash end def super? @scope.superuser? endend
class PostSerializer < ActiveModel::Serializer attributes :title, :body has_many :commentsprivate def attributes hash = super hash.merge!(email: post.email) if super? hash end def super? @scope.superuser? endend
{ post: { title: “Hello Blog!”, body: “This is my first post. Isn’t it fabulous!”, comments: [ { title: “Awesome”, body: “Your first post is great” } ] }}
class CommentSerializer def initialize(comment, scope) @comment, @scope = comment, scope end def serializable_hash { title: @comment.title } end def as_json { comment: serializable_hash } endend
{ post: { title: “Hello Blog!”, body: “This is my first post. Isn’t it fabulous!”, comments: [{ title: “Awesome” }] }}
class PostSerializer < ActiveModel::Serializer attributes :title. :body has_many :commentsprivate def attributes hash = super hash.merge!(email: post.email) if super? hash end def comments post.comments_for(scope) end def super? @scope.superuser? endend
:serializer
option will use implicit detectionto determine a serializer. In this example, you’d have to define two classes: PostSerializer
and AccountSerializer
. You can also add the :serializer
optionto set it explicitly:class UserSerializer < ActiveModel::Serializer has_many :followed_posts, :key => :posts, :serializer => CustomPostSerializer has_one :owne_account, :key => :account, :serializer => PrivateAccountSerializerend
{ post: { id: 1 title: “Hello Blog!”, body: “This is my first post. Isn’t it fabulous!”, comments: [1,2] }, comments: [ { id: 1 title: “Awesome”, body: “Your first post is great” }, { id: 2 title: “Not so awesome”, body: “Why is it so short!” } ]}
class CommentSerializer < ActiveModel::Serializer attributes :id, :title, :body # define any logic for dealing with authorization-based attributes hereendclass PostSerializer < ActiveModel::Serializer attributes :title, :body has_many :comments def as_json { post: serializable_hash }.merge!(associations) end def serializable_hash post_hash = attributes post_hash.merge!(association_ids) post_hash endprivate def attributes hash = super hash.merge!(email: post.email) if super? hash end def comments post.comments_for(scope) end def super? @scope.superuser? endend
class PostSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer attributes :id, :title end # same as before # …end
class PostsController < ApplicationController serialization_scope :current_append
user = get_user # some logic to get the user in questionPostSerializer.new(post, user).to_json # reliably generate JSON output
user = User.new # create a new anonymous userPostSerializer.new(post, user).to_json
{ posts: [ { title: “FIRST POST!”, body: “It’s my first pooooost” }, { title: “Second post!”, body: “Zomg I made it to my second post” } ]}
class ArraySerializer < ActiveModel::ArraySerializer def serializable_array serializers.map do |serializer| serializer.serializable_hash end end def as_json hash = { root => serializable_array } hash.merge!(associations) hash endend
ArraySerializer
with theassociated content and call its serializable_array method. In this case, thoseembedded associations will not recursively include associations.When generating an Array using render json: posts, the controller will invokethe as_json method, which will include its associations and its root.