API's either receive data-modifying requests (post, put, delete) or data-accessing requests (get), and in either case, their response can involve retreiving data, serializing that data into JSON, and returning it.
In Rails, this serialization could be done manually, by, say, creating a serializer method:
# User.rb
def serialize
hash = {}
attributes.each{ |key, val| hash[key] = val }
hash
end
But already, we run into problems of access. What if we don't want to send certain attributes (say, timestamps, or ids, or protected password information)? We could specifically create and continually update hashes of only the desired data. But then what if we want different information to be serialized in different contexts, or for different users? And what if we want non-attribute information (formatted timestamp, or full name, for example)?
Things can quickly grow out of hand.
Rails being Rails, they've got a nice solution that's only a gem away: Active Model Serializers.
So let's add the gem to our Gemfile
:
gem 'active_model_serializers', '~> 0.10.0'
Bundle install, and then, to save some time and thought, let's use the baked-in generators to build our first serializer:
rails g serializer user
That should create a new serializers
folder in app, and a user_serializer.rb
file within it. It'll look something like this:
class UserSerializer < ActiveModel::Serializer
attributes :id
end
So how does this work? We add attributes that we want serialized as arguments to the attributes
method. If we want to add faux-attributes or overwrite attributes with different serialized versions, we add them to the arguments list and then define them in the same file.
The use of object in the below code refers to the object being serialized. In this case, it's a single user.
class UserSerializer < ActiveModel::Serializer
attributes :id, :created_at, :full_name, :email, :bio
# Delegate the practical definition of `full_name` to
# the User model, where it belongs, rather than
# (re)defining it here.
def full_name
object.full_name
end
def created_at
object.created_at.strftime('%B %d, %Y')
end
end
You can test this out in your Ruby console by initializing a new UserSerializer
for a user object:
UserSerializer.new(User.first).as_json
# => "{\"user\":{\"id\":1,\"full_name\":\"Fake Name\",\"email\":\"fake@email.com\",\"created_at\":\"December 31, 2000\"}}"
That was pretty easy, but what if we want varied serializers for varied situations? Let's create a new InsecureUserSerializer
which serializes exactly the information that someone would need to sign in as a user:
class InsecureUserSerializer < ActiveModel::Serializer
attributes :id, :email, :password, :full_name
def full_name
object.full_name
end
end
Just throw this class into the same
serializers
directory.
We can use this one identically in the console.
Well, cool, but what's the point, right?
Serializers' uses become much clearer when creating API controllers. Take a look at this example API users_controller
:
class Api::UsersController < ApiController
def index
return permission_denied_error unless conditions_met
users = User.all
render json: users, each_serializer: InsecureUserSerializer
end
private
def conditions_met
true # We're not calling this an InsecureUserSerializer for nothing
end
end
For more information on this ApiController class (and where that permission_denied_error comes from), take a look at our resource on Cross-Site Request Forgery.
And that's a wrap. All you need to do is make a GET request of the route for the above index
action, and deal with the JSON response:
{
"users": [
{
"id": 1,
"email": "email@fake.com",
"password": "password",
"full_name": "John Smith"
},
{
"id": 2,
"email": "fake@email.com",
"password": "password2",
"full_name": "Sally Smith"
}
]
}