Customizing JSON in your API
Learning Goals
- Generate and customize Rails Serializers
- 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 the building and API video last Thursday during evals. Feel free to use the repository that you created. Otherwise, you can clone this repo. Below are instructions for getting started from scratch.
git clone https://github.com/turingschool-examples/building-apis.git
bundle
git checkout complete-building-api-exercise
bundle exec rake db:create
We want to work with objects that have related models, so let’s add a Store
model:
rails g model store name
rails g model store_book store:references book:references book_price:integer quantity:integer
bundle exec rake db:migrate
Add relationships to your models:
# in book.rb
has_many :store_books
has_many :stores, through: :store_books
# in stores.rb
has_many :store_books
has_many :books, through: :store_books
And whip together a quick seed file:
#in 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
And seed
bundle exec rake db:seed
Create the store controller and routes:
- touch
controllers/api/v1/stores_controller.rb
- add controller setup for index and show
# stores_controller
class Api::V1::StoresController < ApplicationController
def index
end
def show
end
end
- Add the routes to our routes file
namespace :api do namespace :v1 do resources :books resources :stores, only: [:index, :show] end end
- Set
index
andshow
methods to render appropriate json.
# stores_controller
class Api::V1::StoresController < ApplicationController
def index
Store.all
end
def show
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
- Create a Store Serializer and try to build out a hash that will look like this without using a gem:
{ "data": [ { "id": "1", "type": "store", "attributes": { "id": 1, "name": "Toy, Steuber and Schinner", "num_books": 8, "active": true }, "relationships": { "books": { "data": [ { "id": "1", "type": "book" }, { "id": "4", "type": "book" } ] } } } ] }
Using FastJSONAPI to modify as_json
Add this line to your Gemfile.
gem 'fast_jsonapi'
And then bundle install
We can now use the built in generator in order to make ourselves a serializer.
rails g serializer Store id name
This will add the appropriate attributes from the Store model. And give us only the id and store name.
Let’s check out what is in the Serializer.
class StoreSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
end
So now we have this serializer, and we need to modify our controller.
class Api::V1::OrdersController < ApplicationController
def index
render json: StoreSerializer.new(Store.all)
end
def show
render json: StoreSerializer.new(Store.find(params[:id]))
end
end
So what we are doing is instead of rendering the ActiveRecord stuff in json, we are sending it to the serializer, where the stuff gets serialized, and then that gets rendered as json.
But what if we wanted to show some awesome relationship action?
Easy.
class StoreSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
has_many :books
end
Add that to your serializer and refresh.
What if we wanted a custom attribute? We can do so using this format.
Let’s say we wanted an attribute with the number of books.
class StoreSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :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:
class StoreSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
has_many :books
attribute :num_books do |object|
object.books.count
end
attribute :active do
true
end
end
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