Jei
Jei is a simple serializer for Ruby that formats a JSON document described by JSON API.
Installation
Add gem 'jei'
to your application's Gemfile or run gem install jei
to
install it manually.
Usage
Quickstart
require 'jei'
# Create resource serializers.
class ArtistSerializer < Jei::Serializer
attribute :name
has_many :albums
end
class AlbumSerializer < Jei::Serializer
belongs_to :artist
end
artist = Artist.new(id: 1, name: 'FIESTAR', albums: [])
artist.albums << Album.new(id: 1, artist: artist)
artist.albums << Album.new(id: 2, artist: artist)
# Build a JSON API document from the resource.
document = Jei::Document.build(artist)
document.to_json
{
"data": {
"id": "1",
"type": "artists",
"attributes": {
"name": "FIESTAR"
},
"relationships": {
"albums": {
"data": [
{ "id": "1", "type": "albums" },
{ "id": "2", "type": "albums" }
]
}
}
}
}
Serializers
A Serializer
defines what attributes and relationships are serialized in a
document.
Jei uses reflection to automatically find the correct serailizer for a
resource. To do this, create a class that extends Jei::Serializer
and name it
#{resource.class.name}Serializer
. For example, an Artist
resource would
have a matching serializer named ArtistSerializer
in the global namespace.
class Artist; end
class ArtistSerializer < Jei::Serializer; end
Resource Identifiers
A serializer wraps a resource, which has an id and type. By default, the
serializer assumes the resource responds to id
. If not, override
Jei::Serializer#id
and return a custom id as a string.
class ArtistSerializer < Jei::Serializer
def id
resource.uuid
end
end
The default resource type is the resource's lowercased class name with an 's'
appended. For example, Artist
becomes "artists" and Person
becomes
"persons". For a custom type, override Jei::Serializer#type
and return a
string.
class PersonSerializer < Jei::Serializer
def type
'people'.freeze
end
end
Attributes
Attributes represent model data. They are defined in a serializer by using the
attribute
or attributes
methods.
class AlbumSerializer < Jei::Serializer
# Attributes can be listed by name. Each name is used as an attribute key
# and its value is invoked by its name on the resource.
attributes :kind, :name
# Attributes can also be added individually.
attribute :released_on
# This is useful because `attribute` optionally takes a block. It can be
# used to rename an attribute in the document when the resource responds to
# a different name.
attribute(:release_date) { resource.released_on }
# Or it can be use to create entirely new attributes and values.
attribute :formatted_name do
date = resource.released_on.strftime('%Y.%m.%d')
"[#{date}] #{resource.name}"
end
end
album = Album.new(id: 1, kind: :ep, name: 'A Delicate Sense', released_on: Date.new(2016, 3, 9))
Jei::Document.build(album).to_json
{
"data": {
"id": "1",
"type": "albums",
"attributes": {
"kind": "ep",
"name": "A Delicate Sense",
"released_on": "2016-03-09",
"release_date": "2016-03-09",
"formatted_name": "[2016.03.09] A Delicate Sense"
}
}
}
Relationships
Relationships describe how the primary resource relates to other resources. A
one-to-one relationship is defined by the belongs_to
method, and a
one-to-many relationship, has_many
.
The relationship names are invoked on the resource. A belongs-to relationship returns a single resource, whereas has-many returns a collection.
class AlbumSerializer < Jei::Serializer
belongs_to :artist
has_many :tracks
# Like attributes, relationships can also take a block to override its value.
has_many :even_tracks do
resource.tracks.select { |t| t.id.even? }
end
end
class ArtistSerializer < Jei::Serializer; end
class TrackSerializer < Jei::Serializer; end
artist = Artist.new(id: 1)
tracks = [Track.new(id: 1), Track.new(id: 2)]
album = Album.new(id: 1, artist: artist, tracks: tracks)
Jei::Document.build(album).to_json
{
"data": {
"id": "1",
"type": "albums",
"relationships": {
"artist": {
"data": { "id": "1", "type": "artists" }
},
"tracks": {
"data": [
{ "id": "1", "type": "tracks" },
{ "id": "2", "type": "tracks" }
]
},
"even_tracks": {
"data": [
{ "id": "2", "type": "tracks" }
]
}
}
}
}
Options
Each relationship object can be modified with the following options.
-
data
: (Boolean
; default:true
) Setting this tofalse
supresses building a data object with resource identifiers. Note that doing so does not emit a valid JSON API document unless a links or meta object is present.To ensure full linkage, this option is overridden to
true
when the resource is on the included relationship path.class ArtistSerializer < Jei::Serializer has_many :albums, data: false end albums = [Album.new(id: 1), Album.new(id: 2)] artist = Artist.new(id: 1, albums: albums) Jei::Document.build(artist).to_json
{ "data": { "id": "1", "type": "artists", "relationships": { "albums": {} } } }
-
links
: (Proc -> Array<Jei::Link>
) This is for relationship level links. TheProc
must return a list ofLink
s and is run in the context of the serializer.class ArtistSerializer < Jei::Serializer has_many :albums, links: -> { [Jei::Link.new(:related, "/#{type}/#{id}/albums")] } end class AlbumSerializer < Jei::Serializer; end albums = [Album.new(id: 1), Album.new(id: 2)] artist = Artist.new(id: 1, albums: albums) Jei::Document.build(artist).to_json
{ "data": { "id": "1", "type": "artists", "relationships": { "albums": { "data": [ { "id": "1", "type": "albums" }, { "id": "2", "type": "albums" } ], "links": { "related": "/artists/1/albums" } } } } }
-
serializer
: (Class
) Overrides the default serializer used for each related resource.class RecordSerializer < Jei::Serializer def type 'records' end end class ArtistSerializer < Jei::Serializer has_many :albums, serializer: RecordSerializer end artist = Artist.new(id: 1, albums: [Album.new(id: 1)]) Jei::Document.build(artist).to_json
{ "data": { "id": "1", "type": "artists", "relationships": { "albums": { "data": [ { "id": "1", "type": "records" } ] } } } }
Document
As seen in previous examples, Jei::Document
represents a JSON API document.
After building the structure from a resource or collection of resources using
Document.build
, it can be serialized to a Ruby hash (#to_h
) or a JSON
string (#to_json
).
Options
Top level objects can be added using the following options.
-
:errors
: (Array<Hash<Symbol, Object>>
) An array of error objects. Setting this prevents the primary data member from being added to the document.artist = Artist.new(id: 1, name: '') errors = [{ status: '422', source: { pointer: '/data/attributes/name' }, detail: "Name can't be blank" }] Jei::Document.build(artist, errors: errors)
{ "errors": [{ "status": "422", "source": { "pointer": "/data/attributes/name" }, "detail": "Name can't be blank" }] }
-
:fields
: (Hash<String, String>
) A map of resource type-fields that define sparse fieldsets. Keys are resource types, and fields are a comma-separated list of field names. For example,{ 'artists' => 'name,albums', 'albums' => 'released_on' }
.class ArtistSerializer < Jei::Serializer attributes :kind, :name has_many :albums end artist = Artist.new(id: 1, kind: :group, name: 'FIESTAR', albums: []) Jei::Document.build(artist, fields: { 'artists' => 'name' }).to_json
{ "data": { "id": "1", "type": "artists", "attributes": { "name": "FIESTAR" } } }
-
:include
: (String
) A comma separated list of relationship paths. Each path is a list of relationship names, separated by a period. For example, a valid list of paths would beartist,tracks.song
. The set of resources are all unique resources on the include path.class ArtistSerializer < Jei::Serializer attribute :name has_many :albums end class AlbumSerializer < Jei::Serializer attributes :name, :release_date belongs_to :artist has_many :tracks end class TrackSerializer < Jei::Serializer attributes :position, :name belongs_to :album end artist = Artist.new(id: 1, name: 'FIESTAR') album1 = Album.new(id: 1, name: 'A Delicate Sense', release_date: '2016-03-09', artist: artist) album2 = Album.new(id: 2, name: 'Black Label', release_date: '2015-03-04', artist: artist) artist.albums = [album1, album2] album1.tracks = [Track.new(id: 1, position: 2, name: 'Mirror', album: album1)] album2.tracks = [Track.new(id: 2, position: 1, name: "You're Pitiful", album: album2)] Jei::Document.build(artist, include: 'albums.tracks').to_json
{ "data": { "id": "1", "type": "artists", "attributes": { "name": "FIESTAR" }, "relationships": { "albums": { "data": [ { "id": "1", "type": "albums" }, { "id": "2", "type": "albums" } ] } } }, "included": [ { "id": "1", "type": "albums", "attributes": { "name": "A Delicate Sense", "release_date": "2016-03-09" }, "relationships": { "artist": { "data": { "id": "1", "type": "artists" } }, "tracks": { "data": [ { "id": "1", "type": "tracks" } ] } } }, // ... ] }
-
:jsonapi
: (Boolean
) Includes a JSON API object in top level of the document.Jei::Document.build(nil, jsonapi: true).to_json
{ "jsonapi": { "version": "1.0" }, "data": null }
-
:links
: (Array<Link>
) Includes a links object in the top level of the document.links = [ Jei::Link.new(:self, '/artists?page[number]=2'), Jei::Link.new(:prev, '/artists?page[number]=1'), Jei::Link.new(:next, '/artists?page[number]=3') ] Jei::Document.build(nil, links: links).to_json
{ "links": { "self": "/artists?page[number]=2", "prev": "/artists?page[number]=1", "next": "/artists?page[number]=3" }, "data": null }
-
:meta
: (Hash<Symbol, Object>
) Includes a meta object in the top level of the document.Jei::Document.build(nil, meta: { total_pages: 10 }).to_json
{ "meta": { "total_pages": 10 }, "data": null }
-
:serializer
: (Class
) Overrides the default serializer used for the primary resource.class SimpleArtistSerializer < Jei::Serializer attribute :name end artist = Artist.new(id: 1, name: 'FIESTAR') Jei::Document.build(artist, serializer: SimpleArtistSerializer).to_json
{ "data": { "id": "1", "type": "artists", "attributes": { "name": "FIESTAR" } } }
Integration
Jei is not tied to any framework and can be integrated as a normal gem.
Rails
The simplest usage with Rails is to define a new renderer.
# config/initializers/jei.rb
ActionController::Renderers.add(:jsonapi) do |resource, options|
document = Jei::Document.build(resource, options)
json = document.to_json
self.content_type = Mime::Type.lookup_by_extension(:jsonapi)
self.response_body = json
end
# config/initializers/mime_types.rb
Mime::Type.register 'application/vnd.api+json', :jsonapi
Serializers can be placed in app/serializers
. Include Rails' url helpers to
have them conveniently accessible in the serializer context for links.
# app/serializers/application_serializer.rb
class ApplicationSerializer < Jei::Serializer
include Rails.application.routes.url_helpers
end
# app/serializers/album_serializer.rb
class AlbumSerializer < ApplicationSerializer
attributes :kind, :name, :release_date
belongs_to :artist
end
# app/serializers/artist_serializer.rb
class ArtistSerializer < ApplicationSerializer
attributes :name
has_many :albums, data: false, links: -> {
[Jei::Link.new(:related, album_path(resource))]
}
end
Specify the jsonapi
format defined earlier when rendering in a controller.
# app/controllers/artists_controller.rb
class ArtistsController < ApplicationController
def show
artist = Artist.find(params[:id])
render jsonapi: artist, include: params[:include]
end
end