Introducing JsonApiable, Because Building RESTful JSON:APIs in Ruby Should Be Easier

If you are building a REST API on Ruby/Rails and decide to implement it as a JSON:API to avoid bike-shedding, chances are you are using fast_jsonapi or active_model_serializers gem to serialize your resources (if you still haven’t decided which one to use, go with fast_jsonapi). Both are geared towards serializing your resources into JSON responses, but the problem is, neither offers easy-to-use tools to consume API requests, and so you are left to write your own parsers to convert JSON:API relationships and complex attributes in, say, update requests into Rails-friendly params. That’s where JsonApiable enters the picture.

JsonApiable is a new gem that makes it easier for Rails API controllers to handle JSON:API parameter and relationship parsing, strong parameter validation, turning exceptions into well-structured errors and more – all in a Rails-friendly way. JsonApiable doesn’t assume anything about other JSON:API gems you may be using. Feel free to use it in conjunction with fast_jsonapi, active_model_serializer, jsonapi-resources or any other library.

What You Get Out of the Box

After adding JsonApiable to your Gemfile, to get some benefits right away, all you need to do is to include a module it your base API controller:

class API::BaseController < ActionController::Base
  include JsonApiable
end

This one line alone would provide you with a whole bunch of goodies:

Before Actions

Those methods will run automatically before your API controller actions are invoked, providing the following functionality:
1) Ensuring application/vnd.api+json is set as the Content-Type in the request, and returning a well-structured error otherwise
2) Ensuring only valid query parameters are provided in the request URL, returning an error otherwise
3) Parsing page and include query params and making them available to the controller as jsonapi_page and jsonapi_include methods
4) Ensuring application/vnd.api+json is set as the Content-Type in the response

Exception Handling

The following exceptions occurring in your controllers will be caught automatically:
1) ArgumentError, ActionController::UnpermittedParameters – will return 400 Bad Request
2) ActiveRecord::RecordNotFound – will return 404 Not Found (if you use some ORM other than ActiveRecord, you can configure which exception should be raised result in such case).

You can also raise JsonApiable exceptions in your controller and expect them to be caught and returned as appropriate responses:
1) JsonApiable::Errors::MalformedRequestError – will return 400 Bad Request
2) JsonApiable::Errors::UnprocessableEntityError – will return 422 Unprocessable Entity
3) JsonApiable::Errors::UnauthorizedError – will return 401 Unauthorized
4) JsonApiable::Errors::ForbiddenError – will return 403 Forbidden

Full Example

Let’s now see how would we write an API::PostsController#update using JsonApiable. Let’s start with whitelisting the allowed attributes:

Whitelisting Allowed Attributes

class API::PostsController < API::BaseController

 protected

 def jsonapi_allowed_attributes
  [:title, :body,
   meta: [:category, tags: []]
 end
end

This means that if a user will try to update the :published_date attribute, she will get a 400 Bad Request error. Notice that complex attributes are allowed.

Now let’s whitelist the allowed relationships:

Whitelisting Allowed Relationships

class API::PostsController < API::BaseController
  
  protected 

  def jsonapi_allowed_relationships 
    [:author, :comments] 
  end 
end

This means that is a user will try to update any relationship other than :author or :comments, she will get a 400 Bad Request.

Let’s now add the update action:

class API::PostsController < API::BaseController
  def update
    @post = Post.find(params[:id])
    # add some authentication and authorization code here
    @post.update_attributes!(jsonapi_assign_params)
    render json: @post
  end
end

It’s a typical update method, nothing fancy. We don’t even have authentication and authorization code, which is out of the scope of JsonApiable. What we do have, is a nifty little method jsonapi_assign_params. What does is do?

jsonapi_assign_params

jsonapi_assign_params parses JSON:API body of the request and transforms into a Rails-friendly params hash.

Let’s assume we get PATCH /v1/posts/123/update request with the following JSON body:

{ "data":
    { "type": "post",
      "attributes": {
         "title": "My New Title"
     },
     "relationships": {
         "author": {
            "data": {
                 "type": "user",
                 "id": "4528"
            },
           "comments": {
             "data": [
               { "type": "comment", "id": "1489" },
               { "type": "comment", "id": "1490" } 
             ] 
           } 
         }
      } 
   } 
}

Without JsonApiable, you would have to parse that JSON in your server code. Instead, jsonapi_assign_params does the job for you, returning the following hash:

{ "title"=>"My New Title",
  "author_id" => 4528, 
  "comments_attributes"=>{
     "0"=>{"id"=>"1489", "_destroy"=>"false"}, 
     "1"=>{"id"=>"1490", "_destroy"=>"false"}},
     "comment_ids"=>["1489", "1490"],
  }
}

As you can see, the hash is ready to be passed to update_attributes.

One caveat to be aware of: for 1-1 relationships (such as between Post and Author), JsonApiable expects the foreign key to be present in the resource being updated. In other words, the above update code would work only if Post belongs_to to Author, and thus posts table has :author_id field. If you want to allow updating a resource that has_one association with another resource (such as User that has_one Address), the other resource (Address in this example) has to be represented as a complex attribute rather than a relationship.

So jsonapi_assign_params will produce a correct hash for the following JSON:

{ "data":
    { "type": "user",
      "attributes": {
         "address": {
           "street": "123 Broad st.",
           "city": "New York",
           "zip_code": "12345",
           "state_code": "NY",
           "country_code": "US"
         }
     },
}

(Don’t forget to add accepts_nested_attributes_for :address in User model, in this scenario)

But jsonapi_assign_params WON’T product a correct hash for the following JSON:

{ "data":
    { "type": "user",
      "attributes": {
      },
      "relationships": {
         "address": {
              "data": {
                 "type": "address",
                 "id": "456"
              },
         }
   } 
}

Summary

JsonApiable has many more goods, which I will address in the following posts. Meanwhile check the project’s GitHub page – would love to hear your comments and feedback.

Leave a Reply

Your email address will not be published. Required fields are marked *