Introducing JSONAPI::Resources

It's been a long time coming, but the JSON:API spec is nearing a 1.0 release. Dan and I have been actively involved in helping to form this spec, which appeals to us because it is so ambitious and comprehensive. JSON:API goes beyond specifying a format for JSON payloads - it also specifies how data should be fetched and modified. By standardizing so many of the decisions around designing and building an API, JSON:API allows developers to focus on the design of their applications. As JSON:API catches on, we'll all benefit from the standardized tooling that develops for both clients and servers.

Our initial contribution to this tooling is JSONAPI::Resources, or "JR". JR is a gem that allows Rails apps to easily support the JSON:API spec. As you may have guessed from the name, JR is resource-centric. You define resources for your application and JR can automatically fulfill requests to fetch and modify them. JR not only handles the serialization of responses, but also provides controllers that support methods to interact with the resource. By declaring resource relationships you can allow related resources to be retrieved in compound documents, and by specifying resource attributes you can control how the resource is represented to the API client.

Until now, we've been using (and recommending and contributing toward) ActiveModel::Serializers, or "AMS", the Ruby library that has come closest to fulfilling the JSON:API spec. There are a couple reasons that we developed JR instead of continuing to work with AMS. First of all, AMS is not strictly focused on JSON:API, and has been out of compliance for some time. This is not a major hurdle and could be easily overcome by enabling a JSON:API mode in AMS, something which is already under discussion. The primary reason we developed JR is that AMS is focused on serializers and not resources. While serializers are just concerned with the representation of a model, resources can also act as a proxy to a backing model. In this way, JR can assist with fetching and modifying resources, and can therefore handle all aspects of JSON:API.

Components

Let's take a quick look at the major components of JSONAPI::Resources.

Resource

You can define the resources available through your API as subclasses of JSONAPI::Resource . A resource definition looks very similar to the definition of a serializer in AMS. Resources form the core of JR and are used by many of its components.

Let's say we're building a blog. Here's a simple resource definition for posts:

1 2 3 4 5 6 7 require 'json/api/resource' class PostResource < JSONAPI::Resource attributes :id, :title, :body has_one :author end

And here's a corresponding author:

1 2 3 4 5 class AuthorResource < JSONAPI::Resource attributes :id, :name has_many :posts end

All declared attributes can, by default, be accessed through the API. There are also methods on a resource that you can implement to control which of the attributes are creatable, updateable, and fetchable. Any attribute on the underlying object that isn't declared in the resource definition will not be available through the resource.

Relationships support options to set different class names and keys from the resource names. In addition, a relationship can be treated "as a set" for create and update purposes.

In the following example, tags are defined with the acts_as_set option. This allows a collection of tags to be set on a post that will replace all of its existing tags:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class PostResource < JSONAPI::Resource attribute :id attribute :title attribute :body attribute :subject has_one :author, class_name: 'Person' has_one :section has_many :tags, acts_as_set: true has_many :comments def subject @object.title end def self.updateable(keys, context) super(keys - [:author, :subject]) end def self.createable(keys, context) super(keys - [:subject]) end filters :title, :author filter :id end

Also note that the above resource has a computed attribute called subject . This is simply the title of the post, and is not createable or updateable.

A resource also controls which filters its corresponding ResourceController will support. The Resource class provides basic search capabilities, but if you need more control you can implement a find method on your resource.

ResourceSerializer

A ResourceSerializer can serialize a resource into a JSON:API compliant hash, which can then be converted to JSON. ResourceSerializer has a serialize method that takes a resource instance to serialize, and optional fields , includes , and context parameters.

For example:

1 2 post = Post.find(1) JSONAPI::ResourceSerializer.new.serialize(PostResource.new(post))

This returns a hash like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 { posts: [{ id: 1, title: 'New post', body: 'A body!!!', links: { section: nil, author: 1, tags: [1,2,3], comments: [1,2] } }] }

You can also provide some options and filter the fields and include related records. include takes an array of related resources to serialize, and fields takes a hash of resource types to an array of attributes. For example:

1 2 3 4 5 6 7 post = Post.find(1) JSONAPI::ResourceSerializer.new.serialize(PostResource.new(post), ['comments','author','comments.tags','author.posts'], {people: [:id, :email, :comments], posts: [:id, :title, :author], tags: [:name], comments: [:id, :body, :post]})

This outputs:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { :posts=>[ {:id=>1, :title=>"New post", :links=>{:author=>1}} ], :linked=>{ :posts=>[ {:id=>2, :title=>"JR solves your serialization woes!", :links=>{:author=>1}} ], :people=>[ {:id=>1, :email=>"joe@xyz.fake", :links=>{:comments=>[1]}} ], :tags=>[ {:name=>"whiny"}, {:name=>"short"}, {:name=>"happy"} ], :comments=>[ {:id=>1, :body=>"what a dumb post", :links=>{:post=>1}}, {:id=>2, :body=>"i liked it", :links=>{:post=>1}} ] } }

Note that multilevel includes can be specified with a dot notation, like 'author.posts' . In addition, you can control the fields for each resource type.

ResourceController

The ResourceController class provides index , show , create , update , delete , show_association , create_association , and destroy_association methods that function based on the resource definition matching the controller name. The easiest way to use ResourceController is to derive your ApplicationController from it, and in turn derive specific controllers from your ApplicationController . There is no need for any methods to exist in your individual controllers unless you need to alter the default behavior.

1 2 3 4 5 class ApplicationController < JSONAPI::ResourceController end class ContactsController < ApplicationController end

The ResourceController translates the includes and fields parameters from the JSON:API specified style into the internal structures used by the ResourceSerializer .

For example, the following request will get all the contacts with their included phone numbers:

1 http://localhost:3000/contacts?include=phone_numbers&fields[contacts]=name_first,name_last&fields[phone_numbers]=number

The contact records will only contain the first and last names and the phone number will just contain the number.

Of course, you don't need to use ResourceController if your needs are different.

Routing

JR has a couple of helper methods available to assist you with setting up routes.

jsonapi_resources

Like resources in ActionDispatch, jsonapi_resources provides resourceful routes mapping between HTTP verbs and URLs and controller actions. This will also setup mappings for relationship URLs for a resource's associations. For example:

1 2 3 4 5 6 require 'jsonapi/routing_ext' Peeps::Application.routes.draw do jsonapi_resources :contacts jsonapi_resources :phone_numbers end

This generates the following routes:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Prefix Verb URI Pattern Controller#Action contact_links_phone_numbers GET /contacts/:contact_id/links/phone_numbers(.:format) contacts#show_association {:association=>"phone_numbers"} POST /contacts/:contact_id/links/phone_numbers(.:format) contacts#create_association {:association=>"phone_numbers"} DELETE /contacts/:contact_id/links/phone_numbers/:keys(.:format) contacts#destroy_association {:association=>"phone_numbers"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create new_contact GET /contacts/new(.:format) contacts#new edit_contact GET /contacts/:id/edit(.:format) contacts#edit contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy phone_number_links_contact GET /phone_numbers/:phone_number_id/links/contact(.:format) phone_numbers#show_association {:association=>"contact"} POST /phone_numbers/:phone_number_id/links/contact(.:format) phone_numbers#create_association {:association=>"contact"} DELETE /phone_numbers/:phone_number_id/links/contact(.:format) phone_numbers#destroy_association {:association=>"contact"} phone_numbers GET /phone_numbers(.:format) phone_numbers#index POST /phone_numbers(.:format) phone_numbers#create new_phone_number GET /phone_numbers/new(.:format) phone_numbers#new edit_phone_number GET /phone_numbers/:id/edit(.:format) phone_numbers#edit phone_number GET /phone_numbers/:id(.:format) phone_numbers#show PATCH /phone_numbers/:id(.:format) phone_numbers#update PUT /phone_numbers/:id(.:format) phone_numbers#update DELETE /phone_numbers/:id(.:format) phone_numbers#destroy

Use jsonapi_resource for singleton resources that can be looked up without an id.

You can control the relationship routes by passing a block into jsonapi_resources or jsonapi_resource . An empty block will not create any relationship routes.

You can add individual relationship routes with jsonapi_links . For example:

1 2 3 4 jsonapi_resources :posts, except: [:destroy] do jsonapi_link :author, except: [:destroy] jsonapi_links :tags, only: [:show, :create] end

This will create relationship routes for author ( show and create , but not destroy ) and for tags (again show and create , but not destroy ).

Getting started

We've shared a simple contact manager app created with JR called Peeps, which you can pull down and play with (some curl examples are provided). It also contains instructions for how to recreate the app if you want to see just what's involved. Hint - it's not much.

Status and Next Steps

So is JSONAPI::Resources complete? In a word, no. It is functional, but it is missing plenty of features. The biggest, as far as I see them, are pagination, sorting, and support for PATCH operations. Also not yet implemented are the top level meta and links objects. I plan to add these over time and would love community support to make and keep JR as compliant with JSON:API as possible.

In very basic testing I have found JSONAPI::Resources to be anywhere from 20% to 50% faster than the equivalent operation using version 0.8.1 of ActiveModel Serializers and 5% to 20% faster than the 0.9.0alpha master branch. Of course, there's probably some speed that can still be extracted from both projects.

I hope you find JSONAPI::Resources useful for building your own APIs. I'd appreciate your comments as well as your contributions.