Jei

Gem Version Build Status

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 to false 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.

```ruby
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
```

```json
{
  "data": {
    "id": "1",
    "type": "artists",
    "relationships": {
      "albums": {}
    }
  }
}
```
  • links: (Proc -> Array<Jei::Link>) This is for relationship level links. The Proc must return a list of Links 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 be artist,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