Build client libraries compliant with specification defined by
senid231 Merge pull request #285 from senid231/query-params-on-create-and-upda…

allow to send fields and/or includes on create/update resource
Latest commit b4e5804 May 28, 2018

JsonApiClient Build Status Code Climate Code Coverage

This gem is meant to help you build an API client for interacting with REST APIs as laid out by It attempts to give you a query building framework that is easy to understand (it is similar to ActiveRecord scopes).

Note: master is currently tracking the 1.0.0 specification. If you're looking for the older code, see 0.x branch


You will want to create your own resource classes that inherit from JsonApiClient::Resource similar to how you would create an ActiveRecord class. You may also want to create your own abstract base class to share common behavior. Additionally, you will probably want to namespace your models. Namespacing your model will not affect the url routing to that resource.

module MyApi
  # this is an "abstract" base class that
  class Base < JsonApiClient::Resource
    # set the api base url in an abstract base class = ""

  class Article < Base

  class Comment < Base

  class Person < Base

By convention, we guess the resource route from the class name. In the above example, Article's path is "" and Person's path would be "".

Some basic example usage:

MyApi::Article.where(author_id: 1).find(2)
MyApi::Article.where(author_id: 1).all

MyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all

u = "bar", last_name: "foo")

u = MyApi::Person.find(1).first
  a: "b",
  c: "d"

u = MyApi::Person.create(
  a: "b",
  c: "d"

All class level finders/creators should return a JsonApiClient::ResultSet which behaves like an Array and contains extra data about the api response.

Handling Validation Errors

See specification

Out of the box, json_api_client handles server side validation only.

User.create(name: "Bob", email_address: "invalid email")
# => false

user = "Bob", email_address: "invalid email")
# => false

# returns an error collector which is array-like
# => ["Email address is invalid"]

# get all error titles
# => ["Email address is invalid"]

# get errors for a specific parameter
# => ["Email address is invalid"]

user = User.find(1)
user.update_attributes(email_address: "invalid email")
# => false

# => ["Email address is invalid"]

# => "invalid email"

For now we are assuming that error sources are all parameters.

If you want to add client side validation, I suggest creating a form model class that uses ActiveModel's validations.

Meta information

See specification

If the response has a top level meta data section, we can access it via the meta accessor on ResultSet.

# Example response:
  "meta": {
    "copyright": "Copyright 2015 Example Corp.",
    "authors": [
      "Yehuda Katz",
      "Steve Klabnik",
      "Dan Gebhardt"
  "data": {
    // ...
articles = Articles.all

# => "Copyright 2015 Example Corp."

# => ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt"]

Top-level Links

See specification

If the resource returns top level links, we can access them via the links accessor on ResultSet.

articles = Articles.find(1)

Nested Resources

You can force nested resource paths for your models by using a belongs_to association.

Note: Using belongs_to is only necessary for setting a nested path.

module MyApi
  class Account < JsonApiClient::Resource
    belongs_to :user

# try to find without the nested parameter
# => raises ArgumentError

# makes request to /users/2/accounts/1
MyApi::Account.where(user_id: 2).find(1)
# => returns ResultSet

Custom Methods

You can create custom methods on both collections (class method) and members (instance methods).

module MyApi
  class User < JsonApiClient::Resource
    # GET /users/search
    custom_endpoint :search, on: :collection, request_method: :get

    # PUT /users/:id/verify
    custom_endpoint :verify, on: :member, request_method: :put

# makes GET request to /users/search?name=Jeff 'Jeff')
# => <ResultSet of MyApi::User instances>

user = MyApi::User.find(1)
# makes PUT request to /users/1/verify?foo=bar
user.verify(foo: 'bar')

Fetching Includes

See specification

If the response returns a compound document, then we should be able to get the related resources.

# makes request to /articles/1?include=author,
results = Article.includes(:author, :comments => :author).find(1)

# should not have to make additional requests to the server
authors =

# makes POST request to /articles?include=author,
article = 'New one').request_includes(:author, :comments => :author)

# makes PATCH request to /articles/1?include=author,
article = Article.find(1)
article.title = 'Changed'
article.request_includes(:author, :comments => :author)

# request includes will be cleared if response is successful
# to avoid this `keep_request_params` class attribute can be used
Article.keep_request_params = true

# to clear request_includes use

Sparse Fieldsets

See specification

# makes request to /articles?fields[articles]=title,body
article ="title", "body").first

# should have fetched the requested fields
# => "Rails is Omakase"

# should not have returned the created_at
# => raise NoMethodError

# or you can use fieldsets from multiple resources
# makes request to /articles?fields[articles]=title,body&fields[comments]=tag
article ="title", "body",{comments: 'tag'}).first

# makes POST request to /articles?fields[articles]=title,body&fields[comments]=tag
article = 'New one').request_select(:title, :body, comments: 'tag')

# makes PATCH request to /articles/1?fields[articles]=title,body&fields[comments]=tag
article = Article.find(1)
article.title = 'Changed'
article.request_select(:title, :body, comments: 'tag')

# request fields will be cleared if response is successful
# to avoid this `keep_request_params` class attribute can be used
Article.keep_request_params = true

# to clear request fields use
article.reset_request_select!(:comments) # to clear for comments
article.reset_request_select! # to clear for all fields


See specification

# makes request to /people?sort=age
youngest = Person.order(:age).all

# also makes request to /people?sort=age
youngest = Person.order(age: :asc).all

# makes request to /people?sort=-age
oldest = Person.order(age: :desc).all


See specification


# makes request to /articles?page=2&per_page=30
articles =

# also makes request to /articles?page=2&per_page=30
articles = Article.paginate(page: 2, per_page: 30).to_a

# keep in mind that page number can be nil - in that case default number will be applied
# also makes request to /articles?page=1&per_page=30
articles = Article.paginate(page: nil, per_page: 30).to_a

Note: The mapping of pagination parameters is done by the query_builder which is customizable.


If the response contains additional pagination links, you can also get at those:

articles = Article.paginate(page: 2, per_page: 30).to_a

Library compatibility

A JsonApiClient::ResultSet object should be paginatable with both kaminari and will_paginate.


See specifiation

# makes request to /people?filter[name]=Jeff
Person.where(name: 'Jeff').all


You can define schema within your client model. You can define basic types and set default values if you wish. If you declare a basic type, we will try to cast any input to be that type.

The added benefit of declaring your schema is that you can access fields before data is set (otherwise, you'll get a NoMethodError).

Note: This is completely optional. This will set default values and handle typecasting.


class User < JsonApiClient::Resource
  property :name, type: :string
  property :is_admin, type: :boolean, default: false
  property :points_accrued, type: :int, default: 0
  property :averge_points_per_day, type: :float

# default values
u =
# => nil

# => false

# => 0

# casting
u.average_points_per_day = "0.3"
# => 0.3


The basic types that we allow are:

  • :int or :integer
  • :float
  • :string
  • :time - *Note: Include the time zone in the string if it's different than local time.
  • :boolean - Note: we will cast the string version of "true" and "false" to their respective values

Also, we consider nil to be an acceptable value and will not cast the value.

Note : Do not map the primary key as int.



You can customize this path by changing your resource's table_name:

module MyApi
  class SomeResource < Base
    def self.table_name

# requests

Custom headers

You can inject custom headers on resource request by wrapping your code into block:

MyApi::SomeResource.with_headers(x_access_token: 'secure_token_here') do


You can configure your API client to use a custom connection that implementes the run instance method. It should return data that your parser can handle. The default connection class wraps Faraday and lets you add middleware.

class NullConnection
  def initialize(*args)

  def run(request_method, path, params = {}, headers = {})

  def use(*args); end

class CustomConnectionResource < TestResource
  self.connection_class = NullConnection

Connection Options

You can configure your connection using Faraday middleware. In general, you'll want to do this in a base model that all your resources inherit from:

MyApi::Base.connection do |connection|
  # set OAuth2 headers
  connection.use FaradayMiddleware::OAuth2, 'MYTOKEN'

  # log responses
  connection.use Faraday::Response::Logger

  connection.use MyCustomMiddleware

module MyApi
  class User < Base
    # will use the customized connection
Specifying an HTTP Proxy

All resources have a class method connection_options used to pass options to the JsonApiClient::Connection initializer.

MyApi::Base.connection_options[:proxy] = ''
MyApi::Base.connection do |connection|
  # ...

module MyApi
  class User < Base
    # will use the customized connection with proxy

Custom Parser

You can configure your API client to use a custom parser that implements the parse class method. It should return a JsonApiClient::ResultSet instance. You can use it by setting the parser attribute on your model:

class MyCustomParser
  def self.parse(klass, response)
    # returns some ResultSet object

class MyApi::Base < JsonApiClient::Resource
  self.parser = MyCustomParser

Custom Query Builder

You can customize how the scope builder methods map to request parameters.

class MyQueryBuilder
  def initialize(klass); end

  def where(conditions = {})

  # … add order, includes, paginate, page, first, build

class MyApi::Base < JsonApiClient::Resource
  self.query_builder = MyQueryBuilder

Custom Paginator

You can customize how your resources find pagination information from the response.

If the existing paginator fits your requirements but you don't use the default page and per_page params for pagination, you can customise the param keys as follows:

JsonApiClient::Paginating::Paginator.page_param = "page[number]"
JsonApiClient::Paginating::Paginator.per_page_param = "page[size]"

Please note that this is a global configuration, so library authors should create a custom paginator that inherits JsonApiClient::Paginating::Paginator and configure the custom paginator to avoid modifying global config.

If the existing paginator does not fit your needs, you can create a custom paginator:

class MyPaginator
  def initialize(result_set, data); end
  # implement current_page, total_entries, etc

class MyApi::Base < JsonApiClient::Resource
  self.paginator = MyPaginator

Type Casting

You can define your own types and its casting mechanism for schema.

require 'money'
class MyMoneyCaster
  def self.cast(value, default)
    begin, "USD")
    rescue ArgumentError

JsonApiClient::Schema.register money: MyMoneyCaster

and finally

class Order < JsonApiClient::Resource
  property :total_amount, type: :money


Contributions are welcome! Please fork this repo and send a pull request. Your pull request should have:

  • a description about what's broken or what the desired functionality is
  • a test illustrating the bug or new feature
  • the code to fix the bug

Ideally, the PR has 2 commits - the first showing the failed test and the second with the fix - although this is not required. The commits will be squashed into master once accepted.


See changelog