Rails API: JSON Serializer

Let’s take a basic animal shelter API that returns JSON objects about animals. It has an endpoint at /animals that points to the animals#index. Here’s the schema:

create_table "animals" do |t|
t.string "name"
t.integer "age"
t.string "gender"
t.string "species"
t.integer "user_id"
end
create_table "users" do |t|
t.string "username"
t.password_digest "password"
t.string "first_name"
t.string "last_name"
end

Thanks to ActiveRecord associations, we can tell that a user can have many animals. It’s a simple :has_many model relationship. Now let’s see the controller:

class AnimalsController < ApplicationController def index
animals = Animal.all
render json: animals
end
end

Assuming this API is stored on your local machine, if you start the Rails server and navigate to localhost:3000/animals in your browser, you’d see a list of animal objects in JSON format. Check out this JSON Parser extension for Chrome if you don’t already have one. Raw JSON in browser is an eyesore. Assuming your database has animals stored in it (and you’re using your handy-dandy JSON parser), you should see something like this:

[
{
id: 1,
name: "Milo",
age: 5,
species: "Dog",
user_id: 1,
},
{
id: 2,
name: "Cooper",
age: 6,
species: "Dog",
user_id: 2
}
]

Now you may be wondering: Shouldn’t there be timestamps? Pretty sure there are timestamps. The answer is no. Why, you ask? Well, the reasonable answer is because I forgot to include it and don’t feel like typing the answer again by hand. However, the educational answer is because I slipped in a Serializer when you weren’t paying attention!

A JSON Serializer is a class that you create that allows you to customize your JSON structure. Rails has the Fast JSON API gem that does this for you, but since I’m a developer of culture, I like to do them by hand. Through Rails’ implicit pipeline design, using a serializer is as easy as placing the file in the app/services directory, calling it as a new instance in the controller, and chaining methods on that new instance for customization.

Because caring is sharing or something like that. Terrible jokes aside, when using a client application to fetch information from your API, formatting the JSON on the API side makes it that much easier to parse on the client-side. Knowing what your JSON structure will look like before fetching it could save you time in debugger or in the browser console, since you know your Promise will return an array of objects with attributes.

It totally does. In order to get the result I showed you above, it can easily be done on one line without a serializer:

def index
animals = Animal.all
render json: animals, except: [:created_at, :updated_at]
end

This approach is perfectly fine to use, especially if you have a small-scale app with very simple models. However, the Serializer really shines when you have special model relationships and instance methods to be included, trying to fit that all in your controller can end up looking pretty cluttered.

Cue the Animal Serializer

If we navigate to our app directory, let’s create a new directory called services and drop in a new file called animal_serializer.rb. First things first, we declare the class and define the initialize method:

class AnimalSerializer

def initialize(object)
@animal = object
end
end

Great! Now, if you were paying attention earlier, you’d know that we instantiate a new Serializer instance with a model instance as the argument. That’s how Rails will know to read the model and its attributes to follow the formatting. Next, we need a method that is commonly known as to_serialized_json:

def to_serialized_json
options = {
except: [:created_at, :updated_at]
}
@animal.to_json(options)
end

What’s happening here, you ask? Why, exactly what is going on above, just more complicated.

Now that we have the Serializer defined as a class, we simply instantiate it in the controller:

def index
animals = Animal.all
AnimalSerializer.new(animals).to_serialized_json
end

And just like that, we’ve achieved the exact same thing as before but with extra steps! Hooray for being counterproductive!

Simple answer: add more options.

The options defined in the class are what allows you to customize the structure. If we want a very personalized structure that’s easier for us to read, then this becomes immediately worth it. Let’s change up the method some:

def to_serialized_json
options = {
include: [:user],
except: [:created_at, :updated_at]
}
@animal.to_json(options)
end

If you were paying attention (which you’ve been doing great so far) you’ll noticed there’s an include macro inside of our options hash. What that does is change our JSON structure by telling Rails to include the user instance associated with the animal. So now, the JSON object should look more like this:

[
{
id: 1,
name: "Milo",
age: 5,
species: "Dog",
user_id: 1,
user: {
id: 1,
username: "CTD",
password_digest: "a5lqkj4l7k7l4l73l45jl256"
first_name: "Cody",
last_name: "Dupuis"
}
]

Pretty cool, huh? Now we have access to that entire User model instead of just the id.

Now I know what you’re probably thinking. The user instance is missing the timestamps. Why doesn’t anyone care about timestamps anymore? I believe some people do, but I chose to leave those out again. Please, allow me to demonstrate:

def to_serialized_json
options = {
include: {
user: {
except: [:created_at, :updated_at]
}
},
except: [:created_at, :updated_at]
}
@animal.to_json(options)
end

You can even customize nested attributes. So never again will you have to deal with those pesky TIMESTAMPS unless you choose to do so. But the fun doesn’t stop there, what if we have special instance methods we want to include? Let’s take a look at our animal.rb and add an instance method:

class Animal < ApplicationRecord
belongs_to :user
def age_in_months
self.age * 12
end
end

So if I were to open up the Rails console and set a variable equal to the first animal, Milo, I’d have access to this method now that converts his age in years to months. A short example:

milo = Animal.first #=> <ActiveRecord object name="Milo">
milo.age #=> 5
milo.age_in_months #=> 60

So how do I include this new cutting-edge method into the Serializer? Using the methods: macro. Example:

def to_serialized_json
options = {
include: {
user: {
except: [:created_at, :updated_at]
}
},
methods: [:age_in_months],
except: [:created_at, :updated_at]
}
@animal.to_json(options)
end

This newly included method can now be found in your JSON return:

[
{
id: 1,
name: "Milo",
age: 5,
species: "Dog",
user_id: 1,
age_in_months: 60,
user: {
id: 1,
username: "CTD",
password_digest: "a5lqkj4l7k7l4l73l45jl256"
first_name: "Cody",
last_name: "Dupuis"
}
]

Lookin’ good! Additionally you can further customize the association as well, which is the user in this case. I don’t like that the encrypted password string keeps showing up, and I want a built-in method that combines the first name and last name. Open up user.rb and add the full_name method:

class User < ApplicationRecord
has_many :animals
def full_name
self.first_name + self.last_name
end
end

And then navigate back to your animal_serializer.rb to make a couple of changes:

def to_serialized_json
options = {
include: {
user: {
except: [:created_at, :updated_at, :password_digest],
methods: [:full_name]
}
},
methods: [:age_in_months],
except: [:created_at, :updated_at]
}
@animal.to_json(options)
end

Now the final product is some super cool, extra special, fully custom JSON:

[
{
id: 1,
name: "Milo",
age: 5,
species: "Dog",
user_id: 1,
age_in_months: 60,
user: {
id: 1,
username: "CTD",
first_name: "Cody",
last_name: "Dupuis",
full_name: "Cody Dupuis"
}
]

Conclusion

Serializing your JSON makes it easier for you to parse because you’ll have designed the JSON structure yourself. It is best used when you have more complicated model structures or when you have your own methods to include that will make your life a little easier. They are by no means necessary, but can be very helpful when used correctly.

Thank you for taking the time to read this and I hope it was helpful. Happy coding!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store