- Introduce the Fast JSON API gem
- Create serializers using the Fast JSON API gem
- Render related object attributes into JSON data
We've seen that it is entirely possible to create our own service class serializers from scratch. This issue is common enough, though, that there are some popular standardized serializer options available for us to use. In this lesson, we are going to look at one popular option, the Fast JSON API gem and use it to create a close approximation to our JSON data from the previous lessons.
The files in this lesson were populated using the API-only Rails build. Run
rails db:migrate
and rails db:seed
to follow along.
The Fast JSON API is a JSON serializer for Rails APIs. It provides a way for us to generate serializer classes for each resource object in our API that is involved in customized JSON rendering. We can use these serializer classes to define the specific attributes we want objects to share or not share, along with things like related object attributes.
The result is that in our controller actions, rather than writing a custom
render
each time, we write out a serializer for each object once and use Fast
JSON API to control the way our data is structured.
Before we can see the solution Fast JSON API provides, let's look back at the problem we've been dealing with. We will start at the same place we started when creating our own service class serializer. This code-along has three resources set up: birds, locations and sightings:
class Bird < ApplicationRecord
has_many :sightings
has_many :locations, through: :sightings
end
class Location < ApplicationRecord
has_many :sightings
has_many :birds, through: :sightings
end
class Sighting < ApplicationRecord
belongs_to :bird
belongs_to :location
end
We also have one customized controller action:
class SightingsController < ApplicationController
def show
sighting = Sighting.find_by(id: params[:id])
render json: sighting.to_json(:include => {
:bird => {:only => [:name, :species]},
:location => {:only => [:latitude, :longitude]}
}, :except => [:updated_at])
end
end
This produces a specific set of data, with some but not all related attributes included:
{
"id": 2,
"bird_id": 2,
"location_id": 2,
"created_at": "2019-05-14T11:20:37.228Z",
"bird": {
"name": "Grackle",
"species": "Quiscalus Quiscula"
},
"location": {
"latitude": 30.26715,
"longitude": -97.74306
}
}
With just three objects and some minor customization, rendering has become complicated. With Fast JSON API, we can extract and separate this work into Serializer classes, keeping our controller cleaner.
To include Fast JSON API, add gem 'fast_jsonapi'
to your Rails project's Gemfile
and run bundle install
.
Once installed, you will gain access to a new generator, serializer
.
With the new serializer
generator, we can create serializer classes for all
three of our models, which will be available to us in any controller actions
later.
rails g serializer Bird
rails g serializer Location
rails g serializer Sighting
Running the above generators will create a serializers
folder within /app
,
and inside, bird_serializer.rb
, location_serializer.rb
, and
sighting_serializer.rb
. With these serializers, we can start
to define information about each model and their related models we want
to share in our API.
To start using the new serializers, we can update our render json:
statement
so that it initializes the newly created SightingSerializer
, passing in a variable,
just as we did when creating our own service class:
class SightingsController < ApplicationController
def show
sighting = Sighting.find(params[:id])
render json: SightingSerializer.new(sighting)
end
end
ASIDE: Serializers generated by the Fast JSON API gem have two built-in methods called
serializable_hash
andserialized_json
which return a serialized hash and a JSON string respectively. However, we don't actually need either of these in this example, asto_json
will still be called onSightingSerializer.new(sighting)
implicitly. As we will see, once our serializers are configured and initialized, we will not need
to do any additional work
The SightingSerializer.new(sighting)
statement can be used on all SightingController
actions we want to serialize, so if we were to add an index
, for instance, we just
pass in the array of all sightings as well:
def index
sightings = Sighting.all
render json: SightingSerializer.new(sightings)
end
But there is a problem still! If we fire up our Rails server and visit
http://localhost:3000/sightings/2
, all we see is the following:
{
"id": "2",
"type": "sighting"
}
The serializer is working, but it behaves a little differently than we're used to.
When rendering JSON directly, controllers will render all attributes available by
default. These serializers work the other way around - we must always specify
what attributes we want to include. In our example, birds have name
and
species
attributes and locations have latitude
and longitude
attributes,
so to include these we would update both serializers. For sightings, we could
include the created_at
attribute:
class BirdSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :species
end
class LocationSerializer
include FastJsonapi::ObjectSerializer
attributes :latitude, :longitude
end
class SightingSerializer
include FastJsonapi::ObjectSerializer
attributes :created_at
end
If we go back and check http://localhost:3000/sightings/2
again, this time,
we will see that the created_at
attribute is present:
{
"id": "2",
"type": "sighting",
"attributes": {
"created_at": "2019-05-14T16:39:37.011Z"
}
}
We can also use attributes to access related objects, adding them alongside normal object attributes:
class SightingSerializer
include FastJsonapi::ObjectSerializer
attributes :created_at, :bird, :location
end
This results in our rendered JSON including an "attributes"
object with
"created_at"
, "bird"
, and "location"
:
{
"id": "2",
"type": "sighting",
"attributes": {
"created_at": "2019-05-14T16:39:37.011Z",
"bird": {
"id": 2,
"name": "Grackle",
"species": "Quiscalus Quiscula",
"created_at": "2019-05-14T16:39:36.917Z",
"updated_at": "2019-05-14T16:39:36.917Z"
},
"location": {
"id": 2,
"latitude": 30.26715,
"longitude": -97.74306,
"created_at": "2019-05-14T16:39:36.942Z",
"updated_at": "2019-05-14T16:39:36.942Z"
}
}
}
However, here, we have no control over what attributes are included in the
related objects, and so we get all the attributes of "bird"
and
"location"
.
Object relationships can be included in serializers in two steps. The first step is that we include the relationships we want to reflect in our serializers. We can do this in the same way that we include them in the models themselves. A sighting, for instance, belongs to a bird and a location, so we can update the serializer to reflect this:
class SightingSerializer
include FastJsonapi::ObjectSerializer
attributes :created_at
belongs_to :bird
belongs_to :location
end
However, when visiting http://localhost:3000/sightings/2
, Fast JSON API will
display a new "relationships"
object, but will only provide limited
information, including the id of the related object:
{
"id": "2",
"type": "sighting",
"attributes": {
"created_at": "2019-05-14T16:39:37.011Z"
},
"relationships": {
"bird": {
"data": {
"id": "2",
"type": "bird"
}
},
"location": {
"data": {
"id": "2",
"type": "location"
}
}
}
}
Setting these relationships up is necessary for the second step. Now that we
have included relationships connecting the SightingSerializer
to :bird
and
:location
, to include attributes from those objects, the recommended method is
to pass in a second options parameter to the serializer indicating that we want to
include those objects:
def show
sighting = Sighting.find_by(id: params[:id])
options = {
include: [:bird, :location]
}
render json: SightingSerializer.new(sighting, options)
end
The result:
{
"data": {
"id": "2",
"type": "sighting",
"attributes": {
"created_at": "2019-05-14T16:39:37.011Z"
},
"relationships": {
"bird": {
"data": {
"id": "2",
"type": "bird"
}
},
"location": {
"data": {
"id": "2",
"type": "location"
}
}
}
},
"included": [{
"id": "2",
"type": "bird",
"attributes": {
"name": "Grackle",
"species": "Quiscalus Quiscula"
}
},
{
"id": "2",
"type": "location",
"attributes": {
"latitude": 30.26715,
"longitude": -97.74306
}
}
]
}
Because we have a BirdSerializer
and a LocationSerializer
, when including
:bird
and :location
, Fast JSON API will automatically serialize their
attributes as well.
At the beginning of this lesson, we had the following JSON, but with a messy controller:
{
"id": 2,
"bird_id": 2,
"location_id": 2,
"created_at": "2019-05-14T11:20:37.228Z",
"bird": {
"name": "Grackle",
"species": "Quiscalus Quiscula"
},
"location": {
"latitude": 30.26715,
"longitude": -97.74306
}
}
If you recall from when we created our own service class, since we just moved
the to_json
call from the controller action to our serializer, the
JSON data structure looked the same.
Using Fast JSON API, with the use of relationships and passing a second
parameter, we are able to get the same data, but in a much different
structure. Fast JSON API is meant to be flexible and easy to implement, and it
definitely is! From this point in the example bird watching application, we
could fill out Bird
and Location
controllers and when we add in controller
actions, we already have serializers created and ready to use!
In using Fast JSON API though, we lose the ability to design the structure of our JSON data.
There is a lot more you can do with the Fast JSON API gem, and it is worth reading through their documentation to become more familiar with it. It is possible, for instance, to create entirely custom attributes!
What we covered is enough to get us close to where we were creating our own customized serializers. We do not get to choose exactly how data gets serialized the way we do when writing our own serializer classes, but we gain a lot of flexibility by using the Fast JSON API.
The Fast JSON API gem provides a quick way to generate and customize JSON serializers with minimal configuration. Its conventions also allow it to work well even when dealing with a large number of related objects.
Overall, the goal of this section is to get you comfortable enough to get Rails APIs up and running. With practice, it is possible to build a multi-resource API, complete with many serialized JSON rendering endpoints within minutes.
Being able to quickly spin up an API to practice your fetch()
skills is an
excellent way to get familiar with asynchronous requests. As you move towards
building larger frontend projects, you'll also quickly need a place to persist
data and handle things like login security. Rails as an API will be a critical
asset in your development through the remainder of this course.