SimpleJsonapi
A library for building JSON API documents in Ruby. You may also be interested in simple_jsonapi_rails, which provides some integrations for using simple_jsonapi in a Rails application.
To view this README and more documentation of specific classes and methods, view the YARD documentation.
Features
SimpleJsonapi supports the following JSON API features:
- Singular and collection endpoints
- Attributes and relationships, including nested relationships
- Sparse fieldsets (fields parameter)
- Inclusion of related resources (include parameter & included member)
- Links and meta information on the document root, resources, and relationships
- Error objects (errors member)
Other features include:
- Serializers that are easy to define
- Sorting of first-level relationships (sort_related parameter)
What is JSONAPI?
A specification for building APIs in JSON. As its creators write:
If you’ve ever argued with your team about the way your JSON responses should be formatted, JSON API can be your anti-bikeshedding tool.
Here's a sample JSON API response that also sets the stage for the examples we'll see below:
{
"data": [{
"type": "orders",
"id": "1",
"attributes": {
"order_date": "2017-10-15",
"ship_date": null,
"customer_reference": "ABC123"
},
"relationships": {
"customer": {
"data": { "type": "customers", "id": "33" },
"links": {
"self": "http://example.com/orders/1/relationships/customer",
"related": "http://example.com/orders/1/customer"
},
"meta": { "included": true }
},
"products": {
"data": [
{ "type": "products", "id": "7" },
{ "type": "products", "id": "19" }
],
"links": {
"self": "http://example.com/orders/1/relationships/products",
"related": "http://example.com/orders/1/products"
},
"meta": { "included": true }
}
},
"links": {
"self": "http://example.com/orders/1"
}
}],
"included": [{
"type": "customers",
"id": "33",
"attributes": {
"first_name": "Jane",
"last_name": "Doe"
},
"links": {
"self": "http://example.com/customers/33"
}
}, {
"type": "products",
"id": "7",
"attributes": {
"name": "Widget"
},
"links": {
"self": "http://example.com/products/7"
}
}, {
"type": "products",
"id": "19",
"attributes": {
"name": "Gadget"
},
"links": {
"self": "http://example.com/products/19"
}
}],
"links": {
"self": "http://example.com/orders",
"next": "http://example.com/orders?page[number]=2",
"last": "http://example.com/orders?page[number]=10"
},
"meta": {
"generated_at": "2017-11-01T12:34:56Z"
}
}
Installation
If you're using Bundler, just add SimpleJsonapi to your Gemfile:
gem 'simple_jsonapi'
Or run gem install simple_jsonapi
and then add require 'simple_jsonapi'
to your code.
Basic usage
Suppose we have these resource classes.
class Order
include ActiveModel::Model # for the intializer
attr_accessor :id, :order_date, :ship_date, :customer_reference, :customer, :products
end
class Customer
include ActiveModel::Model
attr_accessor :id, :first_name, :last_name
end
class Product
include ActiveModel::Model
attr_accessor :id, :name
end
First we define a serializer for each class.
class OrderSerializer < SimpleJsonapi::Serializer
# `type` and `id` can be inferred from the class name and its `id` method
attributes :order_date, :ship_date, :customer_reference
has_one :customer, serializer: CustomerSerializer
has_many :products, serializer: ProductSerializer do
data { |order| order.products.sort_by(&:name) }
end
link(:self) { |order| "http://example.com/orders/#{order.id}" }
end
class CustomerSerializer < SimpleJsonapi::Serializer
attributes :first_name, :last_name
end
class ProductSerializer < SimpleJsonapi::Serializer
attributes :name
end
Then we can call SimpleJsonapi.render_resource
to render a single resource.
> order = Order.new(id: 1, order_date: Date.new(2017, 10, 15), customer_reference: "ABC123")
> SimpleJsonapi.render_resource(order)
{
:data => {
:id => "1",
:type => "orders",
:attributes => {
:order_date => Sun, 15 Oct 2017,
:ship_date => nil,
:customer_reference => "ABC123"
}
}
}
And we can call SimpleJsonapi.render_resources
to render a collection of resources. render_resources
accepts either a single resource or an Enumerable
and always renders an array.
> SimpleJsonapi.render_resources([order1, order2])
{
:data => [
{
:id => "1",
:type => "orders",
:attributes => { ... }
}, {
:id => "1",
:type => "orders",
:attributes => { ... }
}
]
}
Finally, we can call SimpleJsonapi.render_errors
to render a document with an errors
member. Like render_resources
, render_errors
accepts either a single error or an Enumerable
and always renders an array.
> error = StandardError.new("something wicked this way comes")
> SimpleJsonapi.render_errors(error)
{
:errors => [
{
:code => "standard_error",
:title => "StandardError",
:detail => "something wicked this way comes"
}
]
}
Advanced usage
Type and ID
The type member is inferred from the resource's class name. For example, an Ordering::LineItem
instance's type would be "line_items"
. A serializer can generate a different type by providing a value or a block.
class LineItemSerializer < SimpleJsonapi::Serializer
type "entries"
# or
type { |item| item.class.name.underscore }
end
The id member calls the resource's id
method. A serializer can override the id by providing a value or a block.
class OrderSerializer < SimpleJsonapi::Serializer
id "3.14"
# or
id { |item| item.order_id }
end
Attributes
By default, attributes call the method of the same name on the resource. Serializers can provide custom implementations as well.
class OrderSerializer < SimpleJsonapi::Serializer
attribute :system_version, "1.0"
attribute(:order_date) { |order| order.created_at.to_date }
end
Attributes (and relationships) can be conditionally rendered by providing an if
or unless
parameter. (@current_user
is discussed below)
class UserSerializer < SimpleJsonapi::Serializer
attribute :ssn, if: -> { @current_user.is_an_admin? }
attribute :country, unless: ->(user) { user.hide_demographics? }
end
Resources can have links and meta information.
class OrderSerializer < SimpleJsonapi::Serializer
attributes :order_date, :ship_date, :customer_reference
link(:self) { |order| "https://example.com/orders/#{order.id}" }
(:last_refreshed) { |order| order.updated_at }
end
Relationships
Relationships are defined with has_one
and has_many
; both take the same parameters.
By default, the related resources are retrieved by calling the method of the same name on the resource. Serializers can provide custom implementations as well.
class OrderSerializer < SimpleJsonapi::Serializer
has_one :customer # calls `order.customer`
has_many :products do
data { |order| order.products.sort_by(&:name) }
end
end
By default, a serializer is chosen based on the class of the related resource. Relationships can also specify a serializer to use, or a SerializerInferrer
that will choose an appropriate serializer for ach resource.
class OrderSerializer < SimpleJsonapi::Serializer
has_one :customer, serializer: CustomerSerializer
has_many :products, serializer: ORDERING_SERIALIZER_INFERRER
end
ORDERING_SERIALIZER_INFERRER = SimpleJsonapi::SerializerInferrer.new do |resource|
"Serializers::#{resource.class.name}".safe_constantize
end
Like resources, relationships can have links and meta information.
class OrderSerializer < SimpleJsonapi::Serializer
has_many :products do
link(:self) { |order| "https://example.com/orders/#{order.id}/relationship/products" }
link(:related) { |order| "https://example.com/orders/#{order.id}/products" }
(:sorted_by) { "product_name" }
end
end
Relationships can be conditionally rendered; see the discussion of if
and unless
above under "Attributes".
Sparse fieldsets
The render_resource
and render_resources
methods accept a fields
parameter to filter the list of fields in the rendered resources. The parameter is a hash with object types as keys and comma-delimited lists or arrays of fields as values.
> SimpleJsonapi.render_resource(order,
include: "customer",
fields: {
orders: "order_date,ship_date,customer",
customers: ["last_name", "first_name"],
}
))
Note that if you request a sparse fieldset and an included relationship, the relationship must be in the list of fields.
Including related resources
The render_resource
and render_resources
methods accept an include
parameter to request that specific relationships be rendered under the document's included member.
> SimpleJsonapi.render_resource(order, include: "customer,products")
# or
> SimpleJsonapi.render_resource(order, include: ["customer", "products"])
Serializers can allow the client to request related resources sorted in a specific order via the sort_related
parameter. The sort fields are exposed as an instance variable, @sort
, which is an array of SortFieldSpec
objects.
> SimpleJsonapi.render_resource(order,
include: "products",
sort_related: { products: "-name,id" }
)
class OrderSerializer < SimpleJsonapi::Serializer
has_many :products do
data do |order|
# @sort = [ <SortFieldSpec field=name order=desc>, <SortFieldSpec field=id order=asc> ]
sort_options = @sort.inject({}) do |hash,spec|
hash[spec.field] = spec.order
end
order.products.order(sort_options)
end
end
end
Document links and meta information
Links and meta information can be added to the document root by passing parameters to render_resource
or render_resources
. These parameters must be hashes and are passed through to the rendered document verbatim.
> SimpleJsonapi.render_resources(orders,
links: {
self: "https://example.com/orders"
},
meta: {
generated_at: Time.now
})
Custom methods and extra context
Attributes, relationships, if/unless options, links, meta information, and all other definitions that accept proc evaluate those procs in the context of the serializer instance. This means that any methods defined in the serializer class are also available to the procs.
It is also possible to pass in additional variables at render time via the extras
parameter. Any extra values appear as instance variables on the serializer when the procs are called.
class UserSerializer < SimpleJsonapi::Serializer
attribute :ssn, if: -> { @current_user.is_an_admin? }
relationship :orders do
data { |user| get_orders_for_user(user) }
end
def get_orders_for_user(user)
...
end
end
> SimpleJsonapi.render_resources(orders, extras: { current_user: user })
How it works
There are two primary concepts in SimpleJsonapi: serializer definitions and renderer nodes.
Oddly enough, the serializer class is not the entry point for serializing a document. That's because a document's data member may be a collection of resources of different types, each of which may require a different serializer.
Serializer definitions
The definitions for a serializer are listed below.
NOTE: The
SimpleJsonapi::
prefix is omitted for brevity.
Resource serializer [Serializer]
└─ resource [Definition::Resource]
├─ id [Proc]
├─ type [Proc]
├─ attributes [Hash]
│ └─ attribute [Definition::Attribute]
├─ relationships [Hash]
│ └─ relationship [Definition::Relationship]
│ ├─ data [Proc]
│ ├─ links [Hash]
│ │ └─ link [Definition::ObjectLink]
│ └─ [Hash]
│ └─ member [Definition::ObjectMeta]
├─ links [Hash]
│ └─ link [Definition::ObjectLink]
└─ [Hash]
└─ member [Definition::ObjectMeta]
Error serializer [ErrorSerializer]
└─ error [Definition::Error]
├─ id [Proc]
├─ status [Proc]
├─ code [Proc]
├─ title [Proc]
├─ detail [Proc]
├─ source [Definition::ErrorSource]
│ ├─ parameter [Proc]
│ └─ pointer [Proc]
├─ links [Hash]
│ └─ link [Definition::ObjectLink]
└─ [Hash]
└─ member [Definition::ObjectMeta]
Renderer nodes
The nodes involved in rendering a JSONAPI document are listed below. Entries without a class next to them are rendered by their parent node.
NOTE: The
SimpleJsonapi::
prefix is omitted for brevity.
Resource document [Node::Document::Singular|Collection|Errors]
├─ data [Node::Data::Singular|Collection]
├─ resource [Node::Resource::Full]
│ │ (@serializer is modified here)
│ ├─ id
│ ├─ type
│ ├─ attributes [Node::Attributes]
│ │ └─ attribute
│ ├─ relationships [Node::Relationships]
│ │ └─ relationship [Node::Relationship]
│ │ ├─ data [Node::RelationshipData::Singular|Collection]
│ │ │ │ (@include_spec and @serializer_inferrer are modified here)
│ │ │ ├─ resource linkage [Node::Resource::Linkage]
│ │ │ │ ├─ id
│ │ │ │ ├─ type
│ │ │ │ └─ meta
│ │ │ │ └─ meta member
│ │ │ └─ resource (added to `included` node) [Node::Resource::Full]
│ │ │ └─ (see above for details)
│ │ ├─ links [Node::ObjectLinks]
│ │ │ └─ link
│ │ └─ meta [Node::ObjectMeta]
│ │ └─ meta member
│ ├─ links [Node::ObjectLinks]
│ │ └─ link
│ └─ meta [Node::ObjectMeta]
│ └─ meta member
├─ included [Node::Included]
│ └─ (resources may be rendered here by the relationship data nodes)
├─ links
│ └─ link
└─ meta
└─ meta member
Errors document [Node::Document::Errors]
├─ errors [Node::Errors]
│ ├─ error [Node::Error]
│ │ ├─ id
│ │ ├─ status
│ │ ├─ code
│ │ ├─ title
│ │ ├─ detail
│ │ └─ source [Node::ErrorSource]
│ │ ├─ parameter
│ │ └─ pointer
│ ├─ links [Node::ObjectLinks]
│ │ └─ link
│ └─ meta [Node::ObjectMeta]
│ └─ meta member
├─ links
└─ meta
The following parameters are passed into the top-level document node and through the entire node hierarchy.
- serializer_inferrer is a
SerializerInferrer
instance, used to choose a serializer for each resource. The serializer_inferrer may be replaced at each relationship data node if that relationship has aserializer
parameter. - serializer is a Serializer instance, used when rendering a resource and its child members. The serializer is replaced at each resource node.
- include is an IncludeSpec instance, created from the
include
parameter. It is replaced at each relationship data node. - fields is a FieldSpec instance, created from the
fields
parameter passed toserialize_resource(s)
and available throughout the document. - sort_related is a SortSpec instance, created from the
sort_related
parameter and available throughout the document. - extras is the hash passed to
serialize_resource(s)
. Its values are available to every block as instance variables on the serializer. - root_node is a reference to the document node, used to access the included node.
Contributing
Running the tests:
- Change to the gem's directory
- Run
bundle install
- Run
bundle exec rake test
Release Process
Once pull request is merged to master, on latest master:
- Update CHANGELOG.md. Version: [ major (breaking change: non-backwards compatible release) | minor (new features) | patch (bugfixes) ]
- Update version in lib/global_enforcer/version.rb
- Release by running
bundle exec rake release