JSONAPI::Utils
JSON::Utils is a simple way to get a full-featured JSON API serialization in your controller's responses. This gem works on top of the awesome gem jsonapi-resources, bringing to controllers a Rails-native way to render data.
Installation
Add these lines to your application's Gemfile:
gem 'jsonapi-resources', '~> 0.5.7'
gem 'jsonapi-utils'
And then execute:
$ bundle
Macros
jsonapi_render
: it works like ActionController'srender
method, receiving model objects and rendering them into JSON API's data format.jsonapi_serialize
: in the backstage, it's the method that actually parsers model objects or hashes and builds JSON data. It can be called anywhere in controllers, concerns etc.
Options
Those macros accept the following options:
resource
: explicitly points the resource to be used in the serialization. By default, JSONAPI::Utils will select resources by inferencing from controller's name.
# If in UsersController for some reason it needs to render a different resource:
jsonapi_render json: Post.all, options: { resource: PostResource }
count
: explicitly points the total count of records for the request, in order to build a proper pagination. By default, JSONAPI::Utils will count the total number of records for a given resource.
users = User.all
jsonapi_render json: users, options: { count: users.count }
Usage
Let's say we have a Rails app for a super simple blog.
Models
# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts
validates :first_name, :last_name, presence: true
end
# app/models/user.rb
class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User', foreign_key: 'user_id'
validates :title, :body, presence: true
end
Resources
Here is where we define how the serialization will behave:
# app/resources/user_resource.rb
class UserResource < JSONAPI::Resource
attributes :first_name, :last_name, :full_name, :birthday
attribute :full_name
has_many :posts
def full_name
"#{@model.first_name} #{@model.last_name}"
end
end
# app/resources/post_resource.rb
class PostResource < JSONAPI::Resource
attributes :title, :body
has_one :author
end
Routes & Controllers
Let's define our routes using the jsonapi_resources
and jsonapi_links
macros provied by the jsonapi-resources
gem:
Rails.application.routes.draw do
jsonapi_resources :users do
jsonapi_resources :posts
jsonapi_links :posts
end
end
And a base controller to include the features from jsonapi-resources
and jsonapi-utils
:
# app/controllers/base_controller.rb
class BaseController < JSONAPI::ResourceController
include JSONAPI::Utils
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordNotFound, with: :jsonapi_render_not_found
end
For this example, let's get focused only on read actions. After including JSONAPI::Utils
we can use the jsonapi_render
method
in order to generate responses which follow the JSON API's standards.
# app/controllers/users_controller.rb
class UsersController < BaseController
before_action :load_user, only: [:show]
# GET /users
def index
jsonapi_render json: User.all
end
# GET /users/:id
def show
jsonapi_render json: @user
end
private
def load_user
@user = User.find(params[:id])
end
end
And:
# app/controllers/posts_controller.rb
class PostsController < BaseController
before_action :load_user, only: [:index, :show]
before_action :load_post, only: [:show]
# GET /users/:user_id/posts
def index
posts = @user.posts.enabled
jsonapi_render json: posts, options: { count: posts.count }
end
# GET /users/:user_id/posts/:id
def show
jsonapi_render json: @post
end
private
def load_user
@user = User.find(params[:user_id])
end
def load_post
@post = @user.posts.find(params[:id])
end
end
Erros
Not found
As you might have seen in BaseController this line will handle all errors related to not found resources:
rescue_from ActiveRecord::RecordNotFound, with: :jsonapi_render_not_found
The jsonapi_render_not_found
method will produce the following error payload:
HTTP/1.1 404 Not found
Content-Type: application/vnd.api+json
{
"errors": [
{
"title": "Record not found",
"detail": "The record identified by 3 could not be found.",
"id": null,
"href": null,
"code": 404,
"source": null,
"links": null,
"status": "not_found"
}
]
}
In case you prefer rendering not found resources with null data and 200 OK
status code, you can use jsonapi_render_not_found_with_null
to produce:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": null
}
If you need to create custom error message, check this.
Initializer
In order to enable a proper pagination, record count etc, let's define an initializer such as:
# config/initializers/jsonapi_resources.rb
JSONAPI.configure do |config|
config.json_key_format = :underscored_key
config.route_format = :dasherized_route
config.operations_processor = :active_record
config.allow_include = true
config.allow_sort = true
config.allow_filter = true
config.raise_if_parameters_not_allowed = true
config.default_paginator = :paged
config.top_level_links_include_pagination = true
config.default_page_size = 10
config.maximum_page_size = 20
config. = true
config. = :record_count
config.use_text_errors = false
config.exception_class_whitelist = []
config.always_include_to_one_linkage_data = false
end
You may want a different configuration for your API. For more information check this.
Requests & Responses
Here's some examples of requests – based on those sample controllers – and their respective JSON responses.
- Collection
- Collection (options)
- Single record
- Record (options)
- Relationships (identifier objects)
- Nested resources
Collection
Request:
GET /users HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"type": "users",
"links": {
"self": "http://api.myblog.com/users/1"
},
"attributes": {
"first_name": "Tiago",
"last_name": "Guedes",
"full_name": "Tiago Guedes",
"birthday": null
},
"relationships": {
"posts": {
"links": {
"self": "http://api.myblog.com/users/1/relationships/posts",
"related": "http://api.myblog.com/users/1/posts"
}
}
}
},
{
"id": "2",
"type": "users",
"links": {
"self": "http://api.myblog.com/users/2"
},
"attributes": {
"first_name": "Douglas",
"last_name": "André",
"full_name": "Douglas André",
"birthday": null
},
"relationships": {
"posts": {
"links": {
"self": "http://api.myblog.com/users/2/relationships/posts",
"related": "http://api.myblog.com/users/2/posts"
}
}
}
}
],
"meta": {
"record_count": 2
},
"links": {
"first": "http://api.myblog.com/users?page%5Bnumber%5D=1&page%5Bsize%5D=10",
"last": "http://api.myblog.com/users?page%5Bnumber%5D=1&page%5Bsize%5D=10"
}
}
Collection (options)
Request:
GET /users?include=posts&fields[users]=full_name,posts&fields[posts]=title&page[number]=1&page[size]=1 HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"type": "users",
"links": {
"self": "http://api.myblog.com/users/1"
},
"attributes": {
"full_name": "Tiago Guedes"
},
"relationships": {
"posts": {
"links": {
"self": "http://api.myblog.com/users/1/relationships/posts",
"related": "http://api.myblog.com/users/1/posts"
},
"data": [
{
"type": "posts",
"id": "1"
}
]
}
}
}
],
"included": [
{
"id": "1",
"type": "posts",
"links": {
"self": "http://api.myblog.com/posts/1"
},
"attributes": {
"title": "An awesome post"
}
}
],
"meta": {
"record_count": 2
},
"links": {
"first": "http://api.myblog.com/users?fields%5Bposts%5D=title&fields%5Busers%5D=full_name%2Cposts&include=posts&page%5Bnumber%5D=1&page%5Bsize%5D=1",
"next": "http://api.myblog.com/users?fields%5Bposts%5D=title&fields%5Busers%5D=full_name%2Cposts&include=posts&page%5Bnumber%5D=2&page%5Bsize%5D=1",
"last": "http://api.myblog.com/users?fields%5Bposts%5D=title&fields%5Busers%5D=full_name%2Cposts&include=posts&page%5Bnumber%5D=2&page%5Bsize%5D=1"
}
}
Single record
Request:
GET /users/1 HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"type": "users",
"links": {
"self": "http://api.myblog.com/users/1"
},
"attributes": {
"first_name": "Tiago",
"last_name": "Guedes",
"full_name": "Tiago Guedes",
"birthday": null
},
"relationships": {
"posts": {
"links": {
"self": "http://api.myblog.com/users/1/relationships/posts",
"related": "http://api.myblog.com/users/1/posts"
}
}
}
}
}
Single record (options)
Request:
GET /users/1?include=posts&fields[users]=full_name,posts&fields[posts]=title HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"type": "users",
"links": {
"self": "http://api.myblog.com/users/1"
},
"attributes": {
"full_name": "Tiago Guedes"
},
"relationships": {
"posts": {
"links": {
"self": "http://api.myblog.com/users/1/relationships/posts",
"related": "http://api.myblog.com/users/1/posts"
},
"data": [
{
"type": "posts",
"id": "1"
}
]
}
}
},
"included": [
{
"id": "1",
"type": "posts",
"links": {
"self": "http://api.myblog.com/posts/1"
},
"attributes": {
"title": "An awesome post"
}
}
]
}
Relationships (identifier objects)
Request:
GET /users/1/relationships/posts HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"links": {
"self": "http://api.myblog.com/users/1/relationships/posts",
"related": "http://api.myblog.com/users/1/posts"
},
"data": [
{
"type": "posts",
"id": "1"
}
]
}
Nested resources
Request:
GET /users/1/posts HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"type": "posts",
"links": {
"self": "http://api.myblog.com/posts/1"
},
"attributes": {
"title": "An awesome post",
"body": "Lorem ipsum dolot sit amet"
},
"relationships": {
"author": {
"links": {
"self": "http://api.myblog.com/posts/1/relationships/author",
"related": "http://api.myblog.com/posts/1/author"
}
}
}
}
],
"meta": {
"record_count": 1
},
"links": {
"first": "http://api.myblog.com/posts?page%5Bnumber%5D=1&page%5Bsize%5D=10",
"last": "http://api.myblog.com/posts?page%5Bnumber%5D=1&page%5Bsize%5D=10"
}
}
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake rspec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jsonapi-utils. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.