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.
Add these lines to your application's Gemfile:
gem 'jsonapi-resources', '~> 0.5.7'
gem 'jsonapi-utils'And then execute:
$ bundle-
jsonapi_render: it works like ActionController'srendermethod, 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.
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 }Let's say we have a Rails app for a super simple blog.
# 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
endHere 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
endLet'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
endAnd 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
endFor 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
endAnd:
# 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
endAs 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_foundThe 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.
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.top_level_meta_include_record_count = true
config.top_level_meta_record_count_key = :record_count
config.use_text_errors = false
config.exception_class_whitelist = []
config.always_include_to_one_linkage_data = false
endYou may want a different configuration for your API. For more information check this.
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
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"
}
}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"
}
}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"
}
}
}
}
}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"
}
}
]
}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"
}
]
}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"
}
}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.
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.
The gem is available as open source under the terms of the MIT License.