---
title: Better serialization, less as_json
teaser: Improving the generation of JSON.
tags: web,rails,good code
author: Alex Grant
published_on: 2012-11-27
---

Suppose you have the following in your Rails app:

```ruby
# app/models/user.rb
class User < ActiveRecord::Base
  has_secure_password
  has_many :posts
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    render json: @post.as_json(
      only: [:id, :content, :created_at],
      include: { user: { only: [:id, :username] } }
    )
  end
end
```

Your `show` action returns a <abbr title="JavaScript Object
Notation">JSON</abbr> representation of a post, along with some limited
information about the user it belongs to, using
[as_json](http://apidock.com/rails/ActiveModel/Serializers/JSON/as_json). Works
like a champ.

However... that nested hash you're passing into `as_json` is a bit clunky. It'll
get clunkier if you add more associations, and you'll have to [repeat
yourself](http://en.wikipedia.org/wiki/Don't_repeat_yourself) if you add more
actions that render posts (like `index`).

"A-ha!" you think, "I'll just carefully override `as_json` in my models to
include only the attributes I want by default, and everything will be rainbows
and kittens!"

```ruby
# app/models/user.rb
class User < ActiveRecord::Base
  has_secure_password
  has_many :posts

  def as_json(options = nil)
    super({ only: [:id, :username] }.merge(options || {}))
  end
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user

  def as_json(options = nil)
    super({ only: [:id, :content, :created_at], include: :user }.merge(options || {}))
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  respond_to :json

  def show
    respond_with Post.find(params[:id])
  end
end
```

You fire up a console for a quick sanity check:

    > pp Post.first.as_json
    {"content"=>"test post, please ignore",
     "created_at"=>Thu, 04 Oct 2012 21:05:50 UTC +00:00,
     "id"=>1,
     :user=>
      {"created_at"=>Thu, 04 Oct 2012 17:07:29 UTC +00:00,
       "id"=>1,
       "password_digest"=>
        "$2a$10$NKW95m6zQnPJiaOXz4u5LeMHKnRmrHjLLCUsTu8yMma/XNmJDi6qy",
       "updated_at"=>Thu, 04 Oct 2012 17:07:29 UTC +00:00,
       "username"=>"tester"}}

Cripes! You just exposed the user's password digest! What's going on here?

A bit of Rails source-diving reveals that the original
[as_json](https://github.com/rails/rails/blob/3-2-stable/activemodel/lib/active_model/serializers/json.rb#L89)
you're overriding is a thin wrapper around
[serializable_hash](https://github.com/rails/rails/blob/3-2-stable/activemodel/lib/active_model/serialization.rb#L71),
which does the actual work of serializing attributes and included associations.
The latter is done by, of course, calling `serializable_hash` on the associated
models... not `as_json`.

Armed with this knowledge the quick fix is obvious, and indeed, replacing "`def
as_json`" with "`def serializable_hash`" in the example above will make it work
as intended. But two things about this should make you slightly queasy. For one,
your solution is coupled to the implementation details of an internal Rails
method that could change in the future (what if `serializable_hash` itself
becomes a wrapper like `as_json`?). And for two, serialization seems like it
should be happening closer to the view layer -- that concern doesn't belong in
our model classes.

## active_model_serializers to the rescue

If the <abbr title="JavaScript Object Notation">JSON</abbr> you're emitting is
hairy enough to merit the use of actual templates, you might want something like
[Jbuilder](https://github.com/rails/jbuilder) or
[RABL](https://github.com/nesquena/rabl). But before diving into that world,
consider the underrated
[active_model_serializers](https://github.com/rails-api/active_model_serializers).
It's easy to use and doesn't add much complexity over the old-and-busted
approach -- you just specify the attributes and associations to include in a
different way.

Here's our example again, now using active_model_serializers:

```ruby
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
end

# app/serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer
  has_one :user
  attributes :id, :content, :created_at
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_secure_password
  has_many :posts
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  respond_to :json

  def show
    respond_with Post.find(params[:id])
  end
end
```

The only quirk is the use of `has_one` in serializers for all singular
associations, regardless of whether the real association is `has_one` or
`belongs_to`.

While your integration tests should hit the route and assert that the <abbr
title="JavaScript Object Notation">JSON</abbr> response contains the expected
set of attributes, let's just revisit our sanity check from earlier:

    > pp PostSerializer.new(Post.first).as_json
    {:id=>1,
     :content=>"test post, please ignore",
     :created_at=>Thu, 04 Oct 2012 21:05:50 UTC +00:00,
     :user=>{:id=>1, :username=>"tester"}}

Perfect! Now everything is rainbows and kittens... and you didn't have to
override `as_json`.
