diff --git a/README.md b/README.md new file mode 100644 index 00000000..8e748a3f --- /dev/null +++ b/README.md @@ -0,0 +1,703 @@ +[![Build Status](https://api.travis-ci.org/rails-api/active_model_serializers.png)](https://travis-ci.org/rails-api/active_model_serializers) [![Code Climate](https://codeclimate.com/github/rails-api/active_model_serializers.png)](https://codeclimate.com/github/rails-api/active_model_serializers) [![Coverage Status](https://coveralls.io/repos/rails-api/active_model_serializers/badge.png?branch=master)](https://coveralls.io/r/rails-api/active_model_serializers) + +# Purpose + +The purpose of `ActiveModel::Serializers` is to provide an object to +encapsulate serialization of objects which respond to +read\_attribute\_for\_serialization like ActiveModel ones and including +`ActiveRecord` objects. + +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.** + +# Installing + +The easiest way to install `ActiveModel::Serializers` is to add it to your +`Gemfile`: + +```ruby +gem "active_model_serializers" +``` + +Then, install it on the command line: + +``` +$ bundle install +``` + +#### Ruby 1.8 is no longer supported! + +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: + +```ruby +gem "active_model_serializers", "~> 0.8.0" +``` + + +# 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 +``` + +### Support for POROs + +Currently `ActiveModel::Serializers` expects objects to implement +read\_attribute\_for\_serialization. That's all you need to do to have +your POROs supported. + +# ActiveModel::Serializer + +All new serializers descend from ActiveModel::Serializer + +# render :json + +In your controllers, when you use `render :json`, Rails will now first search +for a serializer for the object and use it if available. + +```ruby +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 `@options` inside. + +To specify a custom serializer for an object, you can specify the +serializer when you render the object: + +```ruby +render json: @post, serializer: FancyPostSerializer +``` + +## Arrays + +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. + +```ruby +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 + +```json +{ + "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: + +```ruby +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 + +```json +[ + { "title": "Post 1", "body": "Hello!" }, + { "title": "Post 2", "body": "Goodbye!" } +] +``` + +To specify a custom serializer for the items within an array: + +```ruby +render json: @posts, each_serializer: FancyPostSerializer +``` + +## Disabling the root element + +You have 4 options to disable the root element, each with a slightly different scope: + +#### 1. Disable root globally for all, or per class + +In an initializer: + +```ruby +ActiveSupport.on_load(:active_model_serializers) do + # Disable for all serializers (except ArraySerializer) + ActiveModel::Serializer.root = false + + # Disable for ArraySerializer + ActiveModel::ArraySerializer.root = false +end +``` + +#### 2. Disable root per render call in your controller + +```ruby +render json: @posts, root: false +``` + +#### 3. Subclass the serializer, and specify using it + +```ruby +class CustomArraySerializer < ActiveModel::ArraySerializer + self.root = false +end + +# controller: +render json: @posts, serializer: CustomArraySerializer +``` + +#### 4. Define default_serializer_options in your controller + +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` + +```ruby +def default_serializer_options + { + root: false + } +end +``` + +## Getting the old version + +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`. + +# Attributes and Associations + +Once you have a serializer, you can specify which attributes and associations +you would like to include in the serialized form. + +```ruby +class PostSerializer < ActiveModel::Serializer + attributes :id, :title, :body + has_many :comments +end +``` + +## Attributes + +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: + +```ruby +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: + +```ruby +class VersionSerializer < ActiveModel::Serializer + attribute :version_object, key: :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](#customizing-scope). + +Serializers provides a method named `filter` 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: + +```ruby +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. + +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: + +```ruby +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: + +```ruby +render json: @posts, serializer: CustomArraySerializer, meta: {total: 10} +``` + +The above usage of `:meta` will produce the following: + +```json +{ + "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: + +```ruby +render json: @posts, serializer: CustomArraySerializer, meta: {total: 10}, meta_key: 'meta_object' +``` + +The above usage of `:meta_key` will produce the following: + +```json +{ + "meta_object": { "total": 10 }, + "posts": [ + { "title": "Post 1", "body": "Hello!" }, + { "title": "Post 2", "body": "Goodbye!" } + ] +} +``` + +If you would like direct, low-level control of attribute serialization, you can +completely override the `attributes` method to return the hash you need: + +```ruby +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 +``` + +## Associations + +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. + +```ruby +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: current_user) + end +end +``` + +As with attributes, you can change the JSON key that the serializer should +use for a particular association. + +```ruby +class PostSerializer < ActiveModel::Serializer + attributes :id, :title, :body + + # look up comments, but use +my_comments+ as the key in JSON + has_many :comments, key: :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: + +```ruby +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 ... + +```ruby +class PostSerializer < ActiveModel::Serializer + attributes :id, :title, :body + has_one :author + has_many :comments + + def filter(keys) + keys.delete :author unless current_user.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.: + +```ruby + 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. + +NOTE: polymorphic was removed because was only supported for has\_one +associations and is in the TODO list of the project. + +## Embedding Associations + +By default, associations will be embedded inside the serialized object. So if +you have a post, the outputted JSON will look like: + +```json +{ + "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: + +```ruby +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: + +```json +{ + "post": { + "id": 1, + "title": "New post", + "body": "A body!", + "comment_ids": [ 1, 2, 3 ] + } +} +``` + +Alternatively, you can choose to embed only the ids or the associated objects per association: + +```ruby +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: + +```json +{ + "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: + +```ruby +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: + +```json +{ + "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" } + ] +} +``` + +You can also specify a different root for the embedded objects than the key +used to reference them: + +```ruby +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: + +```json +{ + "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: + +```ruby +class PostSerializer < ActiveModel::Serializer + embed :ids, include: true + + attributes :id, :title, :body + has_many :comments, embed_key: :external_id +end +``` + +This would generate JSON that would look like this: + +```json +{ + "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. + +## Customizing Scope + +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`: + +```ruby +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: + +```ruby +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: + +```ruby +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, scope_name: :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. + +## Caching + +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: + +```ruby +class PostSerializer < ActiveModel::Serializer + cached # enables caching for this serializer + + attributes :title, :body + + def cache_key + [object, current_user] + end +end +``` + +The caching interface uses `Rails.cache` under the hood. + +# Design and Implementation + +## Keep it Simple + +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. + +## Performance + +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.