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
endBut 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 userThat 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
endSo 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
endYou 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\":\"[email protected]\",\"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
endJust throw this class into the same
serializersdirectory.
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
endFor 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 protected]",
"password": "password",
"full_name": "John Smith"
},
{
"id": 2,
"email": "[email protected]",
"password": "password2",
"full_name": "Sally Smith"
}
]
}