aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_controller.rb1
-rw-r--r--actionpack/lib/action_controller/base.rb1
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb20
-rw-r--r--actionpack/lib/action_controller/metal/serialization.rb26
-rw-r--r--actionpack/test/controller/render_json_test.rb34
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/serializer.rb158
-rw-r--r--activemodel/test/cases/serializer_test.rb406
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_json.rb4
-rw-r--r--railties/guides/source/serializers.textile600
10 files changed, 1238 insertions, 13 deletions
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index f4eaa2fd1b..3829e60bb0 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -31,6 +31,7 @@ module ActionController
autoload :RequestForgeryProtection
autoload :Rescue
autoload :Responder
+ autoload :Serialization
autoload :SessionManagement
autoload :Streaming
autoload :Testing
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 98bfe72fef..cfb9cf5e6e 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -190,6 +190,7 @@ module ActionController
Redirecting,
Rendering,
Renderers::All,
+ Serialization,
ConditionalGet,
RackDelegation,
SessionManagement,
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 0ad9dbeda9..6e9ce450ac 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/blank'
+require 'set'
module ActionController
# See <tt>Renderers.add</tt>
@@ -12,16 +13,13 @@ module ActionController
included do
class_attribute :_renderers
- self._renderers = {}.freeze
+ self._renderers = Set.new.freeze
end
module ClassMethods
def use_renderers(*args)
- new = _renderers.dup
- args.each do |key|
- new[key] = RENDERERS[key]
- end
- self._renderers = new.freeze
+ renderers = _renderers + args
+ self._renderers = renderers.freeze
end
alias use_renderer use_renderers
end
@@ -31,10 +29,10 @@ module ActionController
end
def _handle_render_options(options)
- _renderers.each do |name, value|
- if options.key?(name.to_sym)
+ _renderers.each do |name|
+ if options.key?(name)
_process_options(options)
- return send("_render_option_#{name}", options.delete(name.to_sym), options)
+ return send("_render_option_#{name}", options.delete(name), options)
end
end
nil
@@ -42,7 +40,7 @@ module ActionController
# Hash of available renderers, mapping a renderer name to its proc.
# Default keys are :json, :js, :xml.
- RENDERERS = {}
+ RENDERERS = Set.new
# Adds a new renderer to call within controller actions.
# A renderer is invoked by passing its name as an option to
@@ -79,7 +77,7 @@ module ActionController
# <tt>ActionController::MimeResponds#respond_with</tt>
def self.add(key, &block)
define_method("_render_option_#{key}", &block)
- RENDERERS[key] = block
+ RENDERERS << key.to_sym
end
module All
diff --git a/actionpack/lib/action_controller/metal/serialization.rb b/actionpack/lib/action_controller/metal/serialization.rb
new file mode 100644
index 0000000000..9bb665a9ae
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/serialization.rb
@@ -0,0 +1,26 @@
+module ActionController
+ module Serialization
+ extend ActiveSupport::Concern
+
+ include ActionController::Renderers
+
+ included do
+ class_attribute :_serialization_scope
+ end
+
+ def serialization_scope
+ send(_serialization_scope)
+ end
+
+ def _render_option_json(json, options)
+ json = json.active_model_serializer.new(json, serialization_scope) if json.respond_to?(:active_model_serializer)
+ super
+ end
+
+ module ClassMethods
+ def serialization_scope(scope)
+ self._serialization_scope = scope
+ end
+ end
+ end
+end
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
index fc604a2db3..f886af1a95 100644
--- a/actionpack/test/controller/render_json_test.rb
+++ b/actionpack/test/controller/render_json_test.rb
@@ -15,9 +15,32 @@ class RenderJsonTest < ActionController::TestCase
end
end
+ class JsonSerializer
+ def initialize(object, scope)
+ @object, @scope = object, scope
+ end
+
+ def as_json(*)
+ { :object => @object.as_json, :scope => @scope.as_json }
+ end
+ end
+
+ class JsonSerializable
+ def active_model_serializer
+ JsonSerializer
+ end
+
+ def as_json(*)
+ { :serializable_object => true }
+ end
+ end
+
class TestController < ActionController::Base
protect_from_forgery
+ serialization_scope :current_user
+ attr_reader :current_user
+
def self.controller_path
'test'
end
@@ -61,6 +84,11 @@ class RenderJsonTest < ActionController::TestCase
def render_json_without_options
render :json => JsonRenderable.new
end
+
+ def render_json_with_serializer
+ @current_user = Struct.new(:as_json).new(:current_user => true)
+ render :json => JsonSerializable.new
+ end
end
tests TestController
@@ -132,4 +160,10 @@ class RenderJsonTest < ActionController::TestCase
get :render_json_without_options
assert_equal '{"a":"b"}', @response.body
end
+
+ def test_render_json_with_serializer
+ get :render_json_with_serializer
+ assert_match '"scope":{"current_user":true}', @response.body
+ assert_match '"object":{"serializable_object":true}', @response.body
+ end
end
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index d0e2a6f39c..28765b00bb 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -43,6 +43,7 @@ module ActiveModel
autoload :Observer, 'active_model/observing'
autoload :Observing
autoload :SecurePassword
+ autoload :Serializer
autoload :Serialization
autoload :TestCase
autoload :Translation
diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb
new file mode 100644
index 0000000000..6d0746a3e8
--- /dev/null
+++ b/activemodel/lib/active_model/serializer.rb
@@ -0,0 +1,158 @@
+require "active_support/core_ext/class/attribute"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/module/anonymous"
+require "set"
+
+module ActiveModel
+ class Serializer
+ module Associations
+ class Config < Struct.new(:name, :options)
+ def serializer
+ options[:serializer]
+ end
+ end
+
+ class HasMany < Config
+ def serialize(collection, scope)
+ collection.map do |item|
+ serializer.new(item, scope).serializable_hash
+ end
+ end
+
+ def serialize_ids(collection, scope)
+ # use named scopes if they are present
+ #return collection.ids if collection.respond_to?(:ids)
+
+ collection.map do |item|
+ item.read_attribute_for_serialization(:id)
+ end
+ end
+ end
+
+ class HasOne < Config
+ def serialize(object, scope)
+ object && serializer.new(object, scope).serializable_hash
+ end
+
+ def serialize_ids(object, scope)
+ object && object.read_attribute_for_serialization(:id)
+ end
+ end
+ end
+
+ class_attribute :_attributes
+ self._attributes = Set.new
+
+ class_attribute :_associations
+ self._associations = []
+
+ class_attribute :_root
+ class_attribute :_embed
+ self._embed = :objects
+ class_attribute :_root_embed
+
+ class << self
+ def attributes(*attrs)
+ self._attributes += attrs
+ end
+
+ def associate(klass, attrs)
+ options = attrs.extract_options!
+ self._associations += attrs.map do |attr|
+ unless method_defined?(attr)
+ class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__
+ end
+
+ options[:serializer] ||= const_get("#{attr.to_s.camelize}Serializer")
+ klass.new(attr, options)
+ end
+ end
+
+ def has_many(*attrs)
+ associate(Associations::HasMany, attrs)
+ end
+
+ def has_one(*attrs)
+ associate(Associations::HasOne, attrs)
+ end
+
+ def embed(type, options={})
+ self._embed = type
+ self._root_embed = true if options[:include]
+ end
+
+ def root(name)
+ self._root = name
+ end
+
+ def inherited(klass)
+ return if klass.anonymous?
+
+ name = klass.name.demodulize.underscore.sub(/_serializer$/, '')
+
+ klass.class_eval do
+ alias_method name.to_sym, :object
+ root name.to_sym unless self._root == false
+ end
+ end
+ end
+
+ attr_reader :object, :scope
+
+ def initialize(object, scope)
+ @object, @scope = object, scope
+ end
+
+ def as_json(*)
+ if _root
+ hash = { _root => serializable_hash }
+ hash.merge!(associations) if _root_embed
+ hash
+ else
+ serializable_hash
+ end
+ end
+
+ def serializable_hash
+ if _embed == :ids
+ attributes.merge(association_ids)
+ elsif _embed == :objects
+ attributes.merge(associations)
+ else
+ attributes
+ end
+ end
+
+ def associations
+ hash = {}
+
+ _associations.each do |association|
+ associated_object = send(association.name)
+ hash[association.name] = association.serialize(associated_object, scope)
+ end
+
+ hash
+ end
+
+ def association_ids
+ hash = {}
+
+ _associations.each do |association|
+ associated_object = send(association.name)
+ hash[association.name] = association.serialize_ids(associated_object, scope)
+ end
+
+ hash
+ end
+
+ def attributes
+ hash = {}
+
+ _attributes.each do |name|
+ hash[name] = @object.read_attribute_for_serialization(name)
+ end
+
+ hash
+ end
+ end
+end
diff --git a/activemodel/test/cases/serializer_test.rb b/activemodel/test/cases/serializer_test.rb
new file mode 100644
index 0000000000..00d519dc1a
--- /dev/null
+++ b/activemodel/test/cases/serializer_test.rb
@@ -0,0 +1,406 @@
+require "cases/helper"
+
+class SerializerTest < ActiveModel::TestCase
+ class Model
+ def initialize(hash={})
+ @attributes = hash
+ end
+
+ def read_attribute_for_serialization(name)
+ @attributes[name]
+ end
+ end
+
+ class User < Model
+ attr_accessor :superuser
+
+ def initialize(hash={})
+ super hash.merge(:first_name => "Jose", :last_name => "Valim", :password => "oh noes yugive my password")
+ end
+
+ def super_user?
+ @superuser
+ end
+ end
+
+ class Post < Model
+ attr_accessor :comments
+ end
+
+ class Comment < Model
+ end
+
+ class UserSerializer < ActiveModel::Serializer
+ attributes :first_name, :last_name
+ end
+
+ class User2Serializer < ActiveModel::Serializer
+ attributes :first_name, :last_name
+
+ def serializable_hash
+ attributes.merge(:ok => true).merge(scope)
+ end
+ end
+
+ class MyUserSerializer < ActiveModel::Serializer
+ attributes :first_name, :last_name
+
+ def serializable_hash
+ hash = attributes
+ hash = hash.merge(:super_user => true) if my_user.super_user?
+ hash
+ end
+ end
+
+ class CommentSerializer
+ def initialize(comment, scope)
+ @comment, @scope = comment, scope
+ end
+
+ def serializable_hash
+ { :title => @comment.read_attribute_for_serialization(:title) }
+ end
+
+ def as_json
+ { :comment => serializable_hash }
+ end
+ end
+
+ class PostSerializer < ActiveModel::Serializer
+ attributes :title, :body
+ has_many :comments, :serializer => CommentSerializer
+ end
+
+ def test_attributes
+ user = User.new
+ user_serializer = UserSerializer.new(user, nil)
+
+ hash = user_serializer.as_json
+
+ assert_equal({
+ :user => { :first_name => "Jose", :last_name => "Valim" }
+ }, hash)
+ end
+
+ def test_attributes_method
+ user = User.new
+ user_serializer = User2Serializer.new(user, {})
+
+ hash = user_serializer.as_json
+
+ assert_equal({
+ :user2 => { :first_name => "Jose", :last_name => "Valim", :ok => true }
+ }, hash)
+ end
+
+ def test_serializer_receives_scope
+ user = User.new
+ user_serializer = User2Serializer.new(user, {:scope => true})
+
+ hash = user_serializer.as_json
+
+ assert_equal({
+ :user2 => {
+ :first_name => "Jose",
+ :last_name => "Valim",
+ :ok => true,
+ :scope => true
+ }
+ }, hash)
+ end
+
+ def test_pretty_accessors
+ user = User.new
+ user.superuser = true
+ user_serializer = MyUserSerializer.new(user, nil)
+
+ hash = user_serializer.as_json
+
+ assert_equal({
+ :my_user => {
+ :first_name => "Jose", :last_name => "Valim", :super_user => true
+ }
+ }, hash)
+ end
+
+ def test_has_many
+ user = User.new
+
+ post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
+ comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")]
+ post.comments = comments
+
+ post_serializer = PostSerializer.new(post, user)
+
+ assert_equal({
+ :post => {
+ :title => "New Post",
+ :body => "Body of new post",
+ :comments => [
+ { :title => "Comment1" },
+ { :title => "Comment2" }
+ ]
+ }
+ }, post_serializer.as_json)
+ end
+
+ class Blog < Model
+ attr_accessor :author
+ end
+
+ class AuthorSerializer < ActiveModel::Serializer
+ attributes :first_name, :last_name
+ end
+
+ class BlogSerializer < ActiveModel::Serializer
+ has_one :author, :serializer => AuthorSerializer
+ end
+
+ def test_has_one
+ user = User.new
+ blog = Blog.new
+ blog.author = user
+
+ json = BlogSerializer.new(blog, user).as_json
+ assert_equal({
+ :blog => {
+ :author => {
+ :first_name => "Jose",
+ :last_name => "Valim"
+ }
+ }
+ }, json)
+ end
+
+ def test_implicit_serializer
+ author_serializer = Class.new(ActiveModel::Serializer) do
+ attributes :first_name
+ end
+
+ blog_serializer = Class.new(ActiveModel::Serializer) do
+ const_set(:AuthorSerializer, author_serializer)
+ has_one :author
+ end
+
+ user = User.new
+ blog = Blog.new
+ blog.author = user
+
+ json = blog_serializer.new(blog, user).as_json
+ assert_equal({
+ :author => {
+ :first_name => "Jose"
+ }
+ }, json)
+ end
+
+ def test_overridden_associations
+ author_serializer = Class.new(ActiveModel::Serializer) do
+ attributes :first_name
+ end
+
+ blog_serializer = Class.new(ActiveModel::Serializer) do
+ const_set(:PersonSerializer, author_serializer)
+
+ def person
+ object.author
+ end
+
+ has_one :person
+ end
+
+ user = User.new
+ blog = Blog.new
+ blog.author = user
+
+ json = blog_serializer.new(blog, user).as_json
+ assert_equal({
+ :person => {
+ :first_name => "Jose"
+ }
+ }, json)
+ end
+
+ def post_serializer(type)
+ Class.new(ActiveModel::Serializer) do
+ attributes :title, :body
+ has_many :comments, :serializer => CommentSerializer
+
+ if type != :super
+ define_method :serializable_hash do
+ post_hash = attributes
+ post_hash.merge!(send(type))
+ post_hash
+ end
+ end
+ end
+ end
+
+ def test_associations
+ post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
+ comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")]
+ post.comments = comments
+
+ serializer = post_serializer(:associations).new(post, nil)
+
+ assert_equal({
+ :title => "New Post",
+ :body => "Body of new post",
+ :comments => [
+ { :title => "Comment1" },
+ { :title => "Comment2" }
+ ]
+ }, serializer.as_json)
+ end
+
+ def test_association_ids
+ serializer = post_serializer(:association_ids)
+
+ serializer.class_eval do
+ def as_json(*)
+ { :post => serializable_hash }.merge(associations)
+ end
+ end
+
+ post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
+ comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
+ post.comments = comments
+
+ serializer = serializer.new(post, nil)
+
+ assert_equal({
+ :post => {
+ :title => "New Post",
+ :body => "Body of new post",
+ :comments => [1, 2]
+ },
+ :comments => [
+ { :title => "Comment1" },
+ { :title => "Comment2" }
+ ]
+ }, serializer.as_json)
+ end
+
+ def test_associations_with_nil_association
+ user = User.new
+ blog = Blog.new
+
+ json = BlogSerializer.new(blog, user).as_json
+ assert_equal({
+ :blog => { :author => nil }
+ }, json)
+
+ serializer = Class.new(BlogSerializer) do
+ root :blog
+
+ def serializable_hash
+ attributes.merge(association_ids)
+ end
+ end
+
+ json = serializer.new(blog, user).as_json
+ assert_equal({ :blog => { :author => nil } }, json)
+ end
+
+ def test_custom_root
+ user = User.new
+ blog = Blog.new
+
+ serializer = Class.new(BlogSerializer) do
+ root :my_blog
+ end
+
+ assert_equal({ :my_blog => { :author => nil } }, serializer.new(blog, user).as_json)
+ end
+
+ def test_false_root
+ user = User.new
+ blog = Blog.new
+
+ serializer = Class.new(BlogSerializer) do
+ root false
+ end
+
+ assert_equal({ :author => nil }, serializer.new(blog, user).as_json)
+
+ # test inherited false root
+ serializer = Class.new(serializer)
+ assert_equal({ :author => nil }, serializer.new(blog, user).as_json)
+ end
+
+ def test_embed_ids
+ serializer = post_serializer(:super)
+
+ serializer.class_eval do
+ root :post
+ embed :ids
+ end
+
+ post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
+ comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
+ post.comments = comments
+
+ serializer = serializer.new(post, nil)
+
+ assert_equal({
+ :post => {
+ :title => "New Post",
+ :body => "Body of new post",
+ :comments => [1, 2]
+ }
+ }, serializer.as_json)
+ end
+
+ def test_embed_ids_include_true
+ serializer = post_serializer(:super)
+
+ serializer.class_eval do
+ root :post
+ embed :ids, :include => true
+ end
+
+ post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
+ comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
+ post.comments = comments
+
+ serializer = serializer.new(post, nil)
+
+ assert_equal({
+ :post => {
+ :title => "New Post",
+ :body => "Body of new post",
+ :comments => [1, 2]
+ },
+ :comments => [
+ { :title => "Comment1" },
+ { :title => "Comment2" }
+ ]
+ }, serializer.as_json)
+ end
+
+ def test_embed_objects
+ serializer = post_serializer(:super)
+
+ serializer.class_eval do
+ root :post
+ embed :objects
+ end
+
+ post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
+ comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
+ post.comments = comments
+
+ serializer = serializer.new(post, nil)
+
+ assert_equal({
+ :post => {
+ :title => "New Post",
+ :body => "Body of new post",
+ :comments => [
+ { :title => "Comment1" },
+ { :title => "Comment2" }
+ ]
+ }
+ }, serializer.as_json)
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb
index 14ef27340e..e7dc60a612 100644
--- a/activesupport/lib/active_support/core_ext/object/to_json.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_json.rb
@@ -10,10 +10,10 @@ end
# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
- klass.class_eval <<-RUBY, __FILE__, __LINE__
+ klass.class_eval do
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
- RUBY
+ end
end
diff --git a/railties/guides/source/serializers.textile b/railties/guides/source/serializers.textile
new file mode 100644
index 0000000000..86a5e5ac8d
--- /dev/null
+++ b/railties/guides/source/serializers.textile
@@ -0,0 +1,600 @@
+h2. Rails Serializers
+
+This 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 services
+
+This guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose a
+JSON API that may return different results based on the authorization status of the user.
+
+endprologue.
+
+h3. Serialization
+
+By default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additional
+parameter 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 primary
+way that Rails developers prepared their responses. This works great for simple cases, as the logic for serializing an Active Record object
+is 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 service
+may choose to expose additional information about a resource only if the user is entitled to access it. In addition, a JavaScript front-end
+may 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 doing
+hash-driven-development in your controllers, juggling authorization logic and other concerns, serializers are for you!
+
+h3. The Most Basic Serializer
+
+A basic serializer is a simple Ruby object named after the model class it is serializing.
+
+<ruby>
+class PostSerializer
+ def initialize(post, scope)
+ @post, @scope = post, scope
+ end
+
+ def as_json
+ { post: { title: @post.name, body: @post.body } }
+ end
+end
+</ruby>
+
+A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, the
+authorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer also
+implements 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.
+
+<ruby>
+class PostsController < ApplicationController
+ def show
+ @post = Post.find(params[:id])
+ render json: @post
+ end
+end
+</ruby>
+
+Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer when
+you 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 content
+directly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root.
+
+<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 }
+ end
+end
+</ruby>
+
+h4. Authorization
+
+Let's update our serializer to include the email address of the author of the post, but only if the current user has superuser
+access.
+
+<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
+ end
+
+private
+ def post
+ { title: @post.name, body: @post.body }
+ end
+
+ def super_data
+ { email: @post.email }
+ end
+
+ def super?
+ @scope.superuser?
+ end
+end
+</ruby>
+
+h4. Testing
+
+One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization
+logic in isolation.
+
+<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: "tenderlove@gmail.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
+ end
+end
+</ruby>
+
+It's important to note that serializer objects define a clear interface specifically for serializing an existing object.
+In this case, the serializer expects to receive a post object with +name+, +body+ and +email+ attributes and an authorization
+scope with a +super?+ method.
+
+By defining a clear interface, it's must easier to ensure that your authorization logic is behaving correctly. In this case,
+the serializer doesn't need to concern itself with how the authorization scope decides whether to set the +super?+ flag, just
+whether it is set. In general, you should document these requirements in your serializer files and programatically via tests.
+The documentation library +YARD+ provides excellent tools for describing this kind of requirement:
+
+<ruby>
+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
+</ruby>
+
+h3. Attribute Sugar
+
+To simplify this process for a number of common cases, Rails provides a default superclass named +ActiveModel::Serializer+
+that you can use to implement your serializers.
+
+For example, you will sometimes want to simply include a number of existing attributes from the source model into the outputted
+JSON. In the above example, the +title+ and +body+ attributes were always included in the JSON. Let's see how to use
++ActiveModel::Serializer+ to simplify our post serializer.
+
+<ruby>
+class PostSerializer < ActiveModel::Serializer
+ attributes :title, :body
+
+ def initialize(post, scope)
+ @post, @scope = post, scope
+ end
+
+ def serializable_hash
+ hash = attributes
+ hash.merge!(super_data) if super?
+ hash
+ end
+
+private
+ def super_data
+ { email: @post.email }
+ end
+
+ def super?
+ @scope.superuser?
+ end
+end
+</ruby>
+
+First, we specified the list of included attributes at the top of the class. This will create an instance method called
++attributes+ that extracts those attributes from the post model.
+
+NOTE: Internally, +ActiveModel::Serializer+ uses +read_attribute_for_serialization+, which defaults to +read_attribute+, which defaults to +send+. So if you're rolling your own models for use with the serializer, you can use simple Ruby accessors for your attributes if you like.
+
+Next, we use the attributes methood in our +serializable_hash+ method, which allowed us to eliminate the +post+ method we hand-rolled
+earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializer+ provides a default +as_json+ method for
+us that calls our +serializable_hash+ method and inserts a root. But we can go a step further!
+
+<ruby>
+class PostSerializer < ActiveModel::Serializer
+ attributes :title, :body
+
+private
+ def attributes
+ hash = super
+ hash.merge!(email: post.email) if super?
+ hash
+ end
+
+ def super?
+ @scope.superuser?
+ end
+end
+</ruby>
+
+The superclass provides a default +initialize+ method as well as a default +serializable_hash+ method, which uses
++attributes+. We can call +super+ to get the hash based on the attributes we declared, and then add in any additional
+attributes we want to use.
+
+NOTE: +ActiveModel::Serializer+ will create an accessor matching the name of the current class for the resource you pass in. In this case, because we have defined a PostSerializer, we can access the resource with the +post+ accessor.
+
+h3. Associations
+
+In most JSON APIs, you will want to include associated objects with your serialized object. In this case, let's include
+the comments with the current post.
+
+<ruby>
+class PostSerializer < ActiveModel::Serializer
+ attributes :title, :body
+ has_many :comments
+
+private
+ def attributes
+ hash = super
+ hash.merge!(email: post.email) if super?
+ hash
+ end
+
+ def super?
+ @scope.superuser?
+ end
+end
+</ruby>
+
+The default +serializable_hash+ method will include the comments as embedded objects inside the post.
+
+<javascript>
+{
+ post: {
+ title: "Hello Blog!",
+ body: "This is my first post. Isn't it fabulous!",
+ comments: [
+ {
+ title: "Awesome",
+ body: "Your first post is great"
+ }
+ ]
+ }
+}
+</javascript>
+
+Rails uses the same logic to generate embedded serializations as it does when you use +render :json+. In this case,
+because you didn't define a +CommentSerializer+, Rails used the default +as_json+ on your comment object.
+
+If you define a serializer, Rails will automatically instantiate it with the existing authorization scope.
+
+<ruby>
+class CommentSerializer
+ def initialize(comment, scope)
+ @comment, @scope = comment, scope
+ end
+
+ def serializable_hash
+ { title: @comment.title }
+ end
+
+ def as_json
+ { comment: serializable_hash }
+ end
+end
+</ruby>
+
+If we define the above comment serializer, the outputted JSON will change to:
+
+<javascript>
+{
+ post: {
+ title: "Hello Blog!",
+ body: "This is my first post. Isn't it fabulous!",
+ comments: [{ title: "Awesome" }]
+ }
+}
+</javascript>
+
+Let's imagine that our comment system allows an administrator to kill a comment, and we only want to allow
+users to see the comments they're entitled to see. By default, +has_many :comments+ will simply use the
++comments+ accessor on the post object. We can override the +comments+ accessor to limit the comments used
+to just the comments we want to allow for the current user.
+
+<ruby>
+class PostSerializer < ActiveModel::Serializer
+ attributes :title. :body
+ has_many :comments
+
+private
+ def attributes
+ hash = super
+ hash.merge!(email: post.email) if super?
+ hash
+ end
+
+ def comments
+ post.comments_for(scope)
+ end
+
+ def super?
+ @scope.superuser?
+ end
+end
+</ruby>
+
++ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments
+for the current user.
+
+NOTE: The logic for deciding which comments a user should see still belongs in the model layer. In general, you should encapsulate concerns that require making direct Active Record queries in scopes or public methods on your models.
+
+h3. Customizing Associations
+
+Not all front-ends expect embedded documents in the same form. In these cases, you can override the
+default +serializable_hash+, and use conveniences provided by +ActiveModel::Serializer+ to avoid having to
+build up the hash manually.
+
+For example, let's say our front-end expects the posts and comments in the following format:
+
+<plain>
+{
+ 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!"
+ }
+ ]
+}
+</plain>
+
+We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments.
+
+<ruby>
+class CommentSerializer < ActiveModel::Serializer
+ attributes :id, :title, :body
+
+ # define any logic for dealing with authorization-based attributes here
+end
+
+class 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
+ end
+
+private
+ def attributes
+ hash = super
+ hash.merge!(email: post.email) if super?
+ hash
+ end
+
+ def comments
+ post.comments_for(scope)
+ end
+
+ def super?
+ @scope.superuser?
+ end
+end
+</ruby>
+
+Here, we used two convenience methods: +associations+ and +association_ids+. The first,
++associations+, creates a hash of all of the define associations, using their defined
+serializers. The second, +association_ids+, generates a hash whose key is the association
+name and whose value is an Array of the association's keys.
+
+The +association_ids+ helper will use the overridden version of the association, so in
+this case, +association_ids+ will only include the ids of the comments provided by the
++comments+ method.
+
+h3. Special Association Serializers
+
+So far, associations defined in serializers use either the +as_json+ method on the model
+or the defined serializer for the association type. Sometimes, you may want to serialize
+associated models differently when they are requested as part of another resource than
+when they are requested on their own.
+
+For instance, we might want to provide the full comment when it is requested directly,
+but only its title when requested as part of the post. To achieve this, you can define
+a serializer for associated objects nested inside the main serializer.
+
+<ruby>
+class PostSerializer < ActiveModel::Serializer
+ class CommentSerializer < ActiveModel::Serializer
+ attributes :id, :title
+ end
+
+ # same as before
+ # ...
+end
+</ruby>
+
+In other words, if a +PostSerializer+ is trying to serialize comments, it will first
+look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+
+and finally +comment.as_json+.
+
+h3. Optional Associations
+
+In some cases, you will want to allow a front-end to decide whether to include associated
+content or not. You can achieve this easily by making an association *optional*.
+
+<ruby>
+class PostSerializer < ActiveModel::Serializer
+ attributes :title. :body
+ has_many :comments, :optional => true
+
+ # ...
+end
+</ruby>
+
+If an association is optional, it will not be included unless the request asks for it
+with an +including+ parameter. The +including+ parameter is a comma-separated list of
+optional associations to include. If the +including+ parameter includes an association
+you did not specify in your serializer, it will receive a +401 Forbidden+ response.
+
+h3. Overriding the Defaults
+
+h4. Authorization Scope
+
+By default, the authorization scope for serializers is +:current_user+. This means
+that when you call +render json: @post+, the controller will automatically call
+its +current_user+ method and pass that along to the serializer's initializer.
+
+If you want to change that behavior, simply use the +serialization_scope+ class
+method.
+
+<ruby>
+class PostsController < ApplicationController
+ serialization_scope :current_app
+end
+</ruby>
+
+You can also implement an instance method called (no surprise) +serialization_scope+,
+which allows you to define a dynamic authorization scope based on the current request.
+
+WARNING: If you use different objects as authorization scopes, make sure that they all implement whatever interface you use in your serializers to control what the outputted JSON looks like.
+
+h4. Parameter to Specify Included Optional Associations
+
+In most cases, you should be able to use the default +including+ parameter to specify
+which optional associations to include. If you are already using that parameter name or
+want to reserve it for some reason, you can specify a different name by using the
++serialization_includes_param+ class method.
+
+<ruby>
+class PostsController < ApplicationController
+ serialization_includes_param :associations_to_include
+end
+</ruby>
+
+You can also implement a +serialization_includes+ instance method, which should return an
+Array of optional includes.
+
+WARNING: If you implement +serialization_includes+ and return an invalid association, your user will receive a +401 Forbidden+ exception.
+
+h3. Using Serializers Outside of a Request
+
+The serialization API encapsulates the concern of generating a JSON representation of
+a particular model for a particular user. As a result, you should be able to easily use
+serializers, whether you define them yourself or whether you use +ActiveModel::Serializer+
+outside a request.
+
+For instance, if you want to generate the JSON representation of a post for a user outside
+of a request:
+
+<ruby>
+user = get_user # some logic to get the user in question
+PostSerializer.new(post, user).to_json # reliably generate JSON output
+</ruby>
+
+If you want to generate JSON for an anonymous user, you should be able to use whatever
+technique you use in your application to generate anonymous users outside of a request.
+Typically, that means creating a new user and not saving it to the database:
+
+<ruby>
+user = User.new # create a new anonymous user
+PostSerializer.new(post, user).to_json
+</ruby>
+
+In general, the better you encapsulate your authorization logic, the more easily you
+will be able to use the serializer outside of the context of a request. For instance,
+if you use an authorization library like Cancan, which uses a uniform +user.can?(action, model)+,
+the authorization interface can very easily be replaced by a plain Ruby object for
+testing or usage outside the context of a request.
+
+h3. Collections
+
+So far, we've talked about serializing individual model objects. By default, Rails
+will serialize collections, including when using the +associations+ helper, by
+looping over each element of the collection, calling +serializable_hash+ on the element,
+and then grouping them by their type (using the plural version of their class name
+as the root).
+
+For example, an Array of post objects would serialize as:
+
+<plain>
+{
+ posts: [
+ {
+ title: "FIRST POST!",
+ body: "It's my first pooooost"
+ },
+ { title: "Second post!",
+ body: "Zomg I made it to my second post"
+ }
+ ]
+}
+</plain>
+
+If you want to change the behavior of serialized Arrays, you need to create
+a custom Array serializer.
+
+<ruby>
+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
+ end
+end
+</ruby>
+
+When generating embedded associations using the +associations+ helper inside a
+regular serializer, it will create a new <code>ArraySerializer</code> with the
+associated content and call its +serializable_array+ method. In this case, those
+embedded associations will not recursively include associations.
+
+When generating an Array using +render json: posts+, the controller will invoke
+the +as_json+ method, which will include its associations and its root.