When building RESTful APIs I generally try to use build them using a HATEOAS architecture. JSON API is my preferred specification to adhere to, though previously I was often using Hal+json but found JSON API to gel with me personally more. I am also keen to try and JSON-LD which looks like it could be a game changer but that’s a story for another day and another post.
In a recent RoR project I went looking for a good library to use to implement JSON API in my Rails API. After some research I found that a combination of Netflix fast_jsonapi and restful-jsonapi packages was the best route to take.
Whilst the documentation on the Netflix repositories was fairly good to get me started, when I started doing more complex things I was finding I had to dig through the sourcecode and debug it myself. So I decided to document what I have found for my benefit in the future and hopefully others may also find this helpful.
I’m not going to go into the general setup of these libraries as this can already be found in the readme files. The repositories I’m using are https://github.com/Netflix/fast_jsonapi and https://github.com/Netflix/restful-jsonapi.
Example code is taken from an Agritech application I am currently writing.
Disclaimer: Since I worked out these techniques myself I am unsure if I am fully using the libraries correctly and to their potential. If anyone works out some better ways to use I’d be interested to hear their techniques.
POSTing data
I used the restful-jsonapi library to capture user input from POST request body.
I wanted to be able to send POST requests with the following body to be able to create a farm resource, create a relationship with an existing company and create a new related field resource.
{
"data": {
"attributes": {
"name": "My Farm",
"description": "Farm 1",
},
"relationships": {
"company": {
"data": {"id": "1"}
},
"fields": {
"data": [
{
"attributes": {
"name": "Top Field",
"description": "You know the big at the top.",
}
}]
}
}
}
}
I found the follow controller code was required to achieve this:
class FarmsController < ApplicationController
before_action :set_farm, only: [:show, :edit, :update, :destroy]
# POST /farms
def create
@farm = Farm.new(farm_params_jsonapi)
respond_to do |format|
if @farm.save
format.html { redirect_to @farm, notice: 'Farm was successfully created.' }
format.json {
options = {}
options[:is_collection] = false
render json: FarmSerializer.new(@farm, options).serialized_json
}
else
errors = fetch_errors(@farm)
format.html { render :new }
format.json {
render json: RequestErrorSerializer.new(errors).serialized_json,
status: :unprocessable_entity
}
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_farm
@farm = Farm.find(params[:id])
end
def farm_params
params[:include] ||= []
params.permit([include: []], [page: [:number, :size]], :id, :all)
end
def farm_params_jsonapi
restify_param(:farm).require(:farm)
.permit(
:id,
:name,
:description,
:company_id,
fields_attributes: [
:id,
:name,
:description,
],
fields: [
:id,
:name,
:description,
],
relationships: []
)
end
def fetch_errors
errors = []
entity.errors.each do |key, value|
errors << Error.new(key, value)
end
return errors
end
end
The library will transform the data so that nested related data will be flattened and the property will be appended with ‘_attributes’.
In order to have new records created automatically I found I needed to add the following to my rails model.
class Farm
def fields_attributes=(fieldsData)
self.fields = Field.create(fieldsData)
end
end
Having to add the ‘_attributes’ property to the params hash and having to then add a method to my models was not how I was hoping this would work. But I personally couldn’t find a better way to do this so far and decided that since this worked I would crack on with actually building my API.
Serializers
For transformering and serializing data from a Ruby object to JSON in the correct data shape I used fast_jsonapi.
An example of a serailizer:
class FarmSerializer
include FastJsonapi::ObjectSerializer
attributes :id
attributes :name
attributes :description
has_many :fields
end
An example of this class in use can be seen above in the controller codeblock. The code is fairly simple, just need to include FastJsonapi::ObjectSerializer then define attributes and relationships much like a Rails Model.
Paging
Pagination can be done the following way.
Ensure that you are allowing page params from user input such as
def model_params
params[:include] ||= []
params.permit([include: []], [page: [:number, :size]], :id)
end
To use these parameters you can extract the values and set sensible defaults, passing these to your query:
if params.dig(:page, :number).nil? == false
page_number = params[:page][:number]
else
page_number = 1
end
if params.dig(:page, :size).nil? == false
page_size = params[:page][:size]
else
page_size = 50
end
offset = (page_number.to_i - 1) * page_size.to_i
entities = Farm.all.offset(offset).limit(page_size)
You can then use a query string such as:
/farms?page[number]=2&page[size]=10
Included
JSON API has the ability to switch between including relationships when you make a GET request and not including them.
This can be controlled using a query string parameter of include
.
Example:
/farms?include[]=fields
This endpoint will now return full related field models in the included section of the response
Managing Errors
To manage errors I created a an error serializer and a method to build up an error response for a single record or a collection.
error.rb
class Error
attr_accessor :detail, :title
@detail = []
@title = []
def initialize(title, detail)
@title = title
@detail = detail
end
end
request_error_serializer.rb
class RequestErrorSerializer
include FastJsonapi::ErrorSerializer
attributes :title, :detail
end
error_serializer.rb
require 'fast_jsonapi'
module FastJsonapi::ErrorSerializer
extend ActiveSupport::Concern
included do
attr_accessor :with_root_key
include FastJsonapi::ObjectSerializer
set_id :title
def initialize(resource, options = {})
super
@with_root_key = options[:with_root_key] != false
end
def hash_for_one_record
logger.info super[:data]
serialized_hash = super[:data]&[:attributes]
return !with_root_key ? serialized_hash : {
errors: serialized_hash
}
end
def hash_for_collection
serialized_hash = super[:data]&.map{|err| err[:attributes]}
return !with_root_key ? serialized_hash : {
errors: serialized_hash
}
end
end
end