ActiveModel::Serializer implementation and Rails hooks
Go to file
Benjamin Fleischer af99c0d9e6 Ensure inheritance hooks run
I was seeing transient failures where adapters may not be registered.

e.g. https://travis-ci.org/rails-api/active_model_serializers/builds/77735382

Since we're using the Adapter, JsonApi, and Json classes
as namespaces, some of the conventions we use for modules don't apply.
Basically, we don't want to define the class anywhere besides itself.
Otherwise, the inherited hooks may not run, and some adapters may not
be registered.

For example:

If we have a class Api `class Api; end`
And Api is also used as a namespace for `Api::Product`
And the classes are defined in different files.

In one file:

```ruby
class Api
  autoload :Product
  def self.inherited(subclass)
    puts
    p [:inherited, subclass.name]
    puts
  end
end
```

And in another:

```ruby
class Api
  class Product < Api
    def sell_sell_sell!
      # TODO: sell
    end
  end
end
```

If we load the Api class file first, the inherited hook will be defined on the class
so that when we load the Api::Product class, we'll see the output:

```plain
[ :inherited, Api::Product]
```

However, if we load the Api::Product class first, since it defines the `Api` class
and then inherited from it, the Api file was never loaded, the hook never defined,
and thus never run.

By defining the class as `class Api::Product < Api` We ensure the the Api class
MUST be defined, and thus, the hook will be defined and run and so sunshine and unicorns.

Appendix:

The below would work, but triggers a circular reference warning.
It's also not recommended to mix require with autoload.

```ruby
require 'api'
class Api
  class Product < Api
    def sell_sell_sell!
      # TODO: sell
    end
  end
end
```

This failure scenario was introduced by removing the circular reference warnings in
https://github.com/rails-api/active_model_serializers/pull/1067

Style note:

To make diffs on the adapters smalleer and easier to read, I've maintained the same
identention that was in the original file.  I've decided to prefer ease of reading
the diff over style, esp. since we may later return to the preferred class declaration style.

 with '#' will be ignored, and an empty message aborts the commit.
2015-09-09 08:55:20 -05:00
docs Make Adapters registerable so they are not namespace-constrained 2015-09-08 22:59:36 -05:00
lib Ensure inheritance hooks run 2015-09-09 08:55:20 -05:00
test Ensure inheritance hooks run 2015-09-09 08:55:20 -05:00
.gitignore Make Adapters registerable so they are not namespace-constrained 2015-09-08 22:59:36 -05:00
.rubocop_todo.yml Rubocop: Consistent spacing 2015-09-03 20:51:40 -05:00
.rubocop.yml Ensure inheritance hooks run 2015-09-09 08:55:20 -05:00
.simplecov Add codeclimate test reporter (CI only) 2015-09-06 09:19:07 -05:00
.travis.yml Add Style enforcer (via Rubocop) 2015-09-03 20:50:45 -05:00
active_model_serializers.gemspec Rubocop: Consistent spacing 2015-09-03 20:51:40 -05:00
appveyor.yml removing ruby 2.2 from appveyor 2015-08-26 03:21:30 -03:00
CHANGELOG.md add pagination feature to changelog file 2015-08-18 19:04:03 -03:00
CONTRIBUTING.md Add suggestions from João 2015-03-15 20:26:33 -07:00
Gemfile Make Adapters registerable so they are not namespace-constrained 2015-09-08 22:59:36 -05:00
LICENSE.txt Generate a basic gem 2014-07-05 00:53:48 -04:00
Rakefile Add test coverage; account for no artifacts on CI 2015-09-06 09:19:07 -05:00
README.md Remove url options 2015-09-07 12:13:19 -03:00

ActiveModel::Serializer

Build Status

Windows Build Status - Build status

ActiveModel::Serializer brings convention over configuration to your JSON generation.

AMS does this through two components: serializers and adapters. Serializers describe which attributes and relationships should be serialized. Adapters describe how attributes and relationships should be serialized.

By default AMS will use the Flatten Json Adapter. But we strongly advise you to use JsonApi Adapter that follows 1.0 of the format specified in jsonapi.org/format. Check how to change the adapter in the sections bellow.

RELEASE CANDIDATE, PLEASE READ

This is the master branch of AMS. It will become the 0.10.0 release when it's ready. Currently this is a release candidate. This is not backward compatible with 0.9.0 or 0.8.0.

0.10.x will be based on the 0.8.0 code, but with a more flexible architecture. We'd love your help. Learn how you can help here.

Example

Given two models, a Post(title: string, body: text) and a Comment(name: string, body: text, post_id: integer), you will have two serializers:

class PostSerializer < ActiveModel::Serializer
  cache key: 'posts', expires_in: 3.hours
  attributes :title, :body

  has_many :comments
end

and

class CommentSerializer < ActiveModel::Serializer
  attributes :name, :body

  belongs_to :post
end

Generally speaking, you as a user of AMS will write (or generate) these serializer classes. If you want to use a different adapter, such as a JsonApi, you can change this in an initializer:

ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi

or

ActiveModel::Serializer.config.adapter = :json_api

You won't need to implement an adapter unless you wish to use a new format or media type with AMS.

If you want to have a root key on your responses you should use the Json adapter, instead of the default FlattenJson:

ActiveModel::Serializer.config.adapter = :json

If you would like the key in the outputted JSON to be different from its name in ActiveRecord, you can use the :key option to customize it:

class PostSerializer < ActiveModel::Serializer
  attributes :id, :body

  # look up :subject on the model, but use +title+ in the JSON
  attribute :subject, :key => :title
  has_many :comments
end

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.

Specify a serializer

If you wish to use a serializer other than the default, you can explicitly pass it to the renderer.

1. For a resource:

  render json: @post, serializer: PostPreviewSerializer

2. For an array resource:

# Use the default `ArraySerializer`, which will use `each_serializer` to
# serialize each element
render json: @posts, each_serializer: PostPreviewSerializer

# Or, you can explicitly provide the collection serializer as well
render json: @posts, serializer: CollectionSerializer, each_serializer: PostPreviewSerializer

Meta

If you want a meta attribute in your response, specify it in the render call:

render json: @post, meta: { total: 10 }

The key can be customized using meta_key option.

render json: @post, meta: { total: 10 }, meta_key: "custom_meta"

meta will only be included in your response if you are using an Adapter that supports root, as JsonAPI and Json adapters, the default adapter (FlattenJson) doesn't have root.

Overriding association methods

If you want to override any association, you can use:

class PostSerializer < ActiveModel::Serializer
  attributes :id, :body

  has_many :comments

  def comments
    object.comments.active
  end
end

Overriding attribute methods

If you want to override any attribute, you can use:

class PostSerializer < ActiveModel::Serializer
  attributes :id, :body

  has_many :comments

  def body
    object.body.downcase
  end
end

Built in Adapters

FlattenJSON

It's the default adapter, it generates a json response without a root key. Doesn't follow any specifc convention.

JSON

It also generates a json response but always with a root key. The root key can't be overridden, and will be automatically defined accordingly with the objects being serialized. Doesn't follow any specifc convention.

JSONAPI

This adapter follows 1.0 of the format specified in jsonapi.org/format. It will include the associated resources in the "included" member when the resource names are included in the include option.

  render @posts, include: ['authors', 'comments']
  # or
  render @posts, include: 'authors,comments'

Installation

Add this line to your application's Gemfile:

gem 'active_model_serializers'

And then execute:

$ bundle

Creating a Serializer

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 generated seralizer will contain basic attributes and has_many/has_one/belongs_to declarations, based on the model. For example:

class PostSerializer < ActiveModel::Serializer
  attributes :title, :body

  has_many :comments
  has_one :author
end

and

class CommentSerializer < ActiveModel::Serializer
  attributes :name, :body

  belongs_to :post_id
end

The attribute names are a whitelist of attributes to be serialized.

The has_many, has_one, and belongs_to declarations describe relationships between resources. By default, when you serialize a Post, you will get its Comments as well.

You may also use the :serializer option to specify a custom serializer class, for example:

  has_many :comments, serializer: CommentPreviewSerializer

And you can change the JSON key that the serializer should use for a particular association:

  has_many :comments, key: :reviews

Pagination

Pagination links will be included in your response automatically as long as the resource is paginated using Kaminari or WillPaginate and if you are using a JSON-API adapter.

Although the others adapters does not have this feature, it is possible to implement pagination links to JSON adapter. For more information about it, please see in our docs How to add pagination links

Caching

To cache a serializer, call cache and pass its options. The options are the same options of ActiveSupport::Cache::Store, plus a key option that will be the prefix of the object cache on a pattern "#{key}/#{object.id}-#{object.updated_at}".

The cache support is optimized to use the cached object in multiple request. An object cached on a show request will be reused at the index. If there is a relationship with another cached serializer it will also be created and reused automatically.

[NOTE] Every object is individually cached.

[NOTE] The cache is automatically expired after update an object but it's not deleted.

cache(options = nil) # options: ```{key, expires_in, compress, force, race_condition_ttl}```

Take the example bellow:

class PostSerializer < ActiveModel::Serializer
  cache key: 'post', expires_in: 3.hours
  attributes :title, :body

  has_many :comments
end

On this example every Post object will be cached with the key "post/#{post.id}-#{post.updated_at}". You can use this key to expire it as you want, but in this case it will be automatically expired after 3 hours.

Fragmenting Caching

If there is some API endpoint that shouldn't be fully cached, you can still optimise it, using Fragment Cache on the attributes and relationships that you want to cache.

You can define the attribute by using only or except option on cache method.

[NOTE] Cache serializers will be used at their relationships

Example:

class PostSerializer < ActiveModel::Serializer
  cache key: 'post', expires_in: 3.hours, only: [:title]
  attributes :title, :body

  has_many :comments
end

Getting Help

If you find a bug, please report an Issue.

If you have a question, please post to Stack Overflow.

Thanks!

Contributing

See CONTRIBUTING.md