We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
Customizing JSON in your API
Customizing JSON in Your API
Learning Goals
- Articulate what a serializer is and how to create one in a Rails application.
- Understand the purpose of serializers and how they support OOP Principles
- Understand what constitutes presentation logic in the context of serving a JSON API and why formatting in the model is not the right place
Warmup
On your own, research serializers. In your notebook, write down the answers to these questions:
- What do serializers allow us to do?
- What resources were you able to find? Which seem most promising?
- What search terms did you use that gave the best results?
Serializers
Serializers allow us to break from the concept of views fully with our API, and instead, mold that data in an object-oriented fashion. We don’t have views to do our dirty work for us anymore, so we rely on serializers in order to present to whomever is consuming our API what we want them to see.
When we call render json:
, Rails makes a call to as_json
under the hood unless we have a serializer set up. Eventually, as_json
calls to_json
and our response is generated.
With how we’ve used render json:
up til now, all data related with the resources in our database is sent back to the client as-is.
Let’s imagine that you don’t just want the raw guts of your model converted to JSON and sent out to the user – maybe you want to customize what you send back.
Specifications for JSON Response
Let’s use the json:api specification for our JSON responses. Take a minute to familiarize yourself with the documentation.
- What is the root
key
? - How are the attributes formatted for a resource in a response?
- How are a resource’s relationships formatted?
Exercise
Adding to our Existing Project
You may have created a repo to code-along with from the Building an API in Rails
lesson. Feel free to use the repository that you created. Otherwise, you can clone this)
repo, and use the main
branch. Below are instructions for getting started with this repo.
$ bundle
$ bundle exec rails db:{drop,create,migrate,seed}
We want to work with objects that have related models so we will add a Store
model and connect that to our Book
$ rails g model store name
$ rails g model store_book store:references book:references book_price:integer quantity:integer
$ bundle exec rails db:migrate
And now with our migrations run, let us add the relationships to our models.
app/models/book.rb
has_many :store_books
has_many :stores, through: :store_books
app/models/store.rb
has_many :store_books
has_many :books, through: :store_books
And now, we can whip together a seeds file.
db/seeds.rb
20.times do
Book.create!(
title: Faker::Book.title,
author: Faker::Book.author,
genre: Faker::Book.genre,
summary: Faker::Lorem.paragraph,
number_sold: Faker::Number.within(range: 1..10)
)
end
5.times do
Store.create!(
name: Faker::Company.name
)
end
books = Book.all
books.each do |book|
store_id_1 = rand(1..5)
store_id_2 = rand(1..5)
StoreBook.create!([
{
book_id: book.id,
store_id: store_id_1,
book_price: rand(100..10000),
quantity: rand(1..10)
},
{
book_id: book.id,
store_id: store_id_2,
book_price: rand(100..10000),
quantity: rand(1..10)
}
])
end
Now that we have a seed file, lets actually seed our development database.
$ bundle exec rails db:seed
You can confirm this worked by opening your rails console and taking a look at the contents of your development database.
Now we are going to create a controller for the store and some routes.
$ touch app/controllers/api/v1/stores_controller.rb
app/controllers/api/v1/stores_controller.rb
class Api::V1::StoresController < ApplicationController
def index
end
def show
end
end
And we have to add our routes:
config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :books
resources :stores, only: [:index, :show]
end
namespace :v2 do
resources :books, only: [:index]
end
end
end
And now we have to write the appropriate code in our controller.
api/controllers/api/v1/stores_controller.rb
class Api::V1::StoresController < ApplicationController
def index
render json: Store.all
end
def show
render json: Store.find(params[:id])
end
end
Responses
Use Postman to view the current responses that your API is providing to the routes listed below:
- api/v1/stores
- api/v1/stores/:id
So we have our responses from our server, but it isn’t JSON API 1.0 And it has this created at and updated at stuff which we don’t want. So what do we do? We need to use a serializer.
Customizing JSON
This is some practice time for you.
- Create a serializer for
Store
and build out a hash that will look like the following WITHOUT THE USE OF A GEM. (You only need to do this for the show)
{
"data": [
{
"id": "1",
"type": "store",
"attributes": {
"name": "Toy, Steuber and Schinner",
"num_books": 8
},
"relationships": {
"books": {
"data": [
{
"id": "1",
"type": "book"
},
{
"id": "4",
"type": "book"
}
]
}
}
}
]
}
That was a pain in the butt, wasn’t it? Creating serializers by hand that are JSON API 1.0 for everything we want to make an API for can certainly be time consuming. There are better ways.
Using the jsonapi-serializer gem
You can view the docs on the jsonapi-serializer gem here.
Add it to your Gemfile
gem "jsonapi-serializer"
And go ahead and
$ bundle install
This gem gives us a built in generator to make ourselves all of the serializers we could possibly want.
$ rails g serializer Store name
We can take a look and see what’s inside it.
app/serializers/store_serializer.rb
class StoreSerializer
include JSONAPI::Serializer
attributes :name
end
Now that we have our serializer, we need to edit our controller to use said serializer.
app/controllers/api/v1/stores_controller.rb
class Api::V1::StoresController < ApplicationController
def index
render json: StoreSerializer.new(Store.all)
end
def show
render json: StoreSerializer.new(Store.find(params[:id]))
end
end
We can see that instead of just letting our ActiveRecord be rendered in JSON, we are going to take our collections, and send them to the serializer, and have the result of THAT be converted to JSON.
What we have here is all fine, but it still lacks our relationship information. We don’t know anything about books that belong to the particular stores.
First, we add a line to our serializer.
app/serializers/store_serializer.rb
class StoreSerializer
include JSONAPI::Serializer
attributes :name
has_many :books
end
If we try to run Postman again, it fails and tells us that it’s trying to look for a serializer for our book, but it can’t find one, so it is up to us to create one.
$ rails g serializer Book title author genre summary number_sold
Restart our server and we should start to see some books.
We also have the ability to add our own custom attributes. What if we wanted an attribute that told us how many books each store had?
app/serializers/store_serializer.rb
class StoreSerializer
include JSONAPI::Serializer
attributes :name
has_many :books
attribute :num_books do |object|
object.books.count
end
end
This syntax is a bit different from what we are used to. We use attribute
singular, and then as a symbol we pick the name of what we want our attribute to be. We use a do end block similar to an enumerable with a block parameter. Now the block parameter, object
is a lot like self. We get to use it for each single thing of a collection we pass to the serializer. We are essentially saying for each thing you serialize, grab the books and count them too. In this manner we can add a custom generated value for each book.
We can also have a custom static attribute like so:
app/serializers/store_serializer.rb
class StoreSerializer
include JSONAPI::Serializer
attributes :name
has_many :books
attribute :num_books do |object|
object.books.count
end
attribute :active do
true
end
end
Alternatively we could create a num_books method in our Store
model and then set it as an attribute in our serializer:
app/models/store.rb
class Store < ApplicationRecord
has_many :store_books
has_many :books, through: :store_books
def num_books
self.books.count
end
end
app/serializers/store_serializer.rb
class StoreSerializer
include JSONAPI::Serializer
attributes :name, :num_books
has_many :books
attribute :active do
true
end
end
Completed version of this lesson to this point is available here.
Extra Practice
Do what we did to Stores
, but for Books
now.
- Some existing fields
id
,title
,author
,genre
,summary
,num_sold
- Some custom fields
num_stores
- A relationship
stores