JSONAPI::Resources, or "JR", provides a framework for developing a server that complies with the JSON API specification.
Like JSON API itself, JR's design is focused on the resources served by an API. JR needs little more than a definition of your resources, including their attributes and relationships, to make your server compliant with JSON API.
JR is designed to work with Rails 4.0+, and provides custom routes, controllers, and serializers. JR's resources may be backed by ActiveRecord models or by custom objects.
We have a simple demo app, called Peeps, available to show how JR is used.
JSON API maintains a (non-verified) listing of client libraries which should be compatible with JSON API compliant server implementations such as JR.
Add JR to your application's Gemfile:
gem 'jsonapi-resources' And then execute:
$ bundle Or install it yourself as:
$ gem install jsonapi-resources Resources define the public interface to your API. A resource defines which attributes are exposed, as well as relationships to other resources.
Resource definitions should by convention be placed in a directory under app named resources, app/resources. The class name should be the single underscored name of the model that backs the resource with _resource.rb appended. For example, a Contact model's resource should have a class named ContactResource defined in a file named contact_resource.rb.
Resources must be derived from JSONAPI::Resource, or a class that is itself derived from JSONAPI::Resource.
For example:
class ContactResource < JSONAPI::Resource endAny of a resource's attributes that are accessible must be explicitly declared. Single attributes can be declared using the attribute method, and multiple attributes can be declared with the attributes method on the resource class.
For example:
class ContactResource < JSONAPI::Resource attribute :name_first attributes :name_last, :email, :twitter endThis resource has 4 defined attributes: name_first, name_last, email, twitter, as well as the automatically defined attributes id and type. By default these attributes must exist on the model that is handled by the resource.
A resource object wraps a Ruby object, usually an ActiveModel record, which is available as the @model variable. This allows a resource's methods to access the underlying model.
For example, a computed attribute for full_name could be defined as such:
class ContactResource < JSONAPI::Resource attributes :name_first, :name_last, :email, :twitter attribute :full_name def full_name "#{@model.name_first}, #{@model.name_last}" end endBy default all attributes are assumed to be fetchable. The list of fetchable attributes can be filtered by overriding the fetchable_fields method.
Here's an example that prevents guest users from seeing the email field:
class AuthorResource < JSONAPI::Resource attributes :name, :email model_name 'Person' has_many :posts def fetchable_fields if (context.current_user.guest) super(context) - [:email] else super(context) end end endContext flows through from the controller and can be used to control the attributes based on the current user (or other value).
By default all attributes are assumed to be updatable and creatable. To prevent some attributes from being accepted by the update or create methods, override the self.updatable_fields and self.creatable_fields methods on a resource.
This example prevents full_name from being set:
class ContactResource < JSONAPI::Resource attributes :name_first, :name_last, :full_name def full_name "#{@model.name_first}, #{@model.name_last}" end def self.updatable_fields(context) super - [:full_name] end def self.creatable_fields(context) super - [:full_name] end endThe context is not by default used by the ResourceController, but may be used if you override the controller methods. By using the context you have the option to determine the creatable and updatable fields based on the user.
JR supports sorting primary resources by multiple sort criteria.
By default all attributes are assumed to be sortable. To prevent some attributes from being sortable, override the self.sortable_fields method on a resource.
Here's an example that prevents sorting by post's body:
class PostResource < JSONAPI::Resource attributes :title, :body def self.sortable_fields(context) super(context) - [:body] end endAttributes can have a Format. By default all attributes use the default formatter. If an attribute has the format option set the system will attempt to find a formatter based on this name. In the following example the last_login_time will be returned formatted to a certain time zone:
class PersonResource < JSONAPI::Resource attributes :name, :email attribute :last_login_time, format: :date_with_timezone endThe system will lookup a value formatter named DateWithTimezoneValueFormatter and will use this when serializing and updating the attribute. See the Value Formatters section for more details.
Resources are always represented using a key of id. The resource will interrogate the model to find the primary key. If the underlying model does not use id as the primary key and does not support the primary_key method you must use the primary_key method to tell the resource which field on the model to use as the primary key. Note: this must be the actual primary key of the model.
By default only integer values are allowed for primary key. To change this behavior you can override verify_key class method:
class CurrencyResource < JSONAPI::Resource primary_key :code attributes :code, :name has_many :expense_entries def self.verify_key(key, context = nil) key && String(key) end endThe name of the underlying model is inferred from the Resource name. It can be overridden by use of the model_name method. For example:
class AuthorResource < JSONAPI::Resource attribute :name model_name 'Person' has_many :posts endRelated resources need to be specified in the resource. These may be declared with the relationship or the has_one and the has_many methods.
Here's a simple example using the relationship method where a post has a single author and an author can have many posts:
class PostResource < JSONAPI::Resource attribute :title, :body relationship :author, to: :one endAnd the corresponding author:
class AuthorResource < JSONAPI::Resource attribute :name relationship :posts, to: :many endAnd here's the equivalent resources using the has_one and has_many methods:
class PostResource < JSONAPI::Resource attribute :title, :body has_one :author endAnd the corresponding author:
class AuthorResource < JSONAPI::Resource attribute :name has_many :posts endThe relationship methods (relationship, has_one, and has_many) support the following options:
class_name- a string specifying the underlying class for the related resourceforeign_key- the method on the resource used to fetch the related resource. Defaults to<resource_name>_idfor has_one and<resource_name>_idsfor has_many relationships.acts_as_set- allows the entire set of related records to be replaced in one operation. Defaults to false if not set.polymorphic- set to true to identify relationships that are polymorphic.relation_name- the name of the relation to use on the model. A lambda may be provided which allows conditional selection of the relation based on the context.
to_one relationships support the additional option:
foreign_key_on- defaults to:self. To indicate that the foreign key is on the related resource specify:related.
Examples:
class CommentResource < JSONAPI::Resource attributes :body has_one :post has_one :author, class_name: 'Person' has_many :tags, acts_as_set: true end class ExpenseEntryResource < JSONAPI::Resource attributes :cost, :transaction_date has_one :currency, class_name: 'Currency', foreign_key: 'currency_code' has_one :employee end class TagResource < JSONAPI::Resource attributes :name has_one :taggable, polymorphic: true endclass BookResource < JSONAPI::Resource # Only book_admins may see unapproved comments for a book. Using # a lambda to select the correct relation on the model has_many :book_comments, relation_name: -> (options = {}) { context = options[:context] current_user = context ? context[:current_user] : nil unless current_user && current_user.book_admin :approved_book_comments else :book_comments end } ... endThe polymorphic relationship will require the resource and controller to exist, although routing to them will cause an error.
class TaggableResource < JSONAPI::Resource; end class TaggablesController < JSONAPI::ResourceController; endFilters for locating objects of the resource type are specified in the resource definition. Single filters can be declared using the filter method, and multiple filters can be declared with the filters method on the resource class.
For example:
class ContactResource < JSONAPI::Resource attributes :name_first, :name_last, :email, :twitter filter :id filters :name_first, :name_last endThen a request could pass in a filter for example http://example.com/contacts?filter[name_last]=Smith and the system will find all people where the last name exactly matches Smith.
A default filter may be defined for a resource using the default option on the filter method. This default is used unless the request overrides this value.
For example:
class CommentResource < JSONAPI::Resource attributes :body, :status has_one :post has_one :author filter :status, default: 'published,pending' endThe default value is used as if it came from the request.
Basic finding by filters is supported by resources. This is implemented in the find and find_by_key finder methods. Currently this is implemented for ActiveRecord based resources. The finder methods rely on the records method to get an Arel relation. It is therefore possible to override records to affect the three find related methods.
If you need to change the base records on which find and find_by_key operate, you can override the records method on the resource class.
For example to allow a user to only retrieve his own posts you can do the following:
class PostResource < JSONAPI::Resource attribute :title, :body def self.records(options = {}) context = options[:context] context.current_user.posts end endWhen you create a relationship, a method is created to fetch record(s) for that relationship. This method calls records_for(relationship_name) by default.
class PostResource < JSONAPI::Resource has_one :author has_many :comments # def record_for_author(options = {}) # records_for("author", options) # end # def records_for_comments(options = {}) # records_for("comments", options) # end endFor example, you may want raise an error if the user is not authorized to view the related records.
class BaseResource < JSONAPI::Resource def records_for(relationship_name, options={}) context = options[:context] records = model.public_send(relationship_name) unless context.current_user.can_view?(records) raise NotAuthorizedError end records end endThe apply_filter method is called to apply each filter to the Arel relation. You may override this method to gain control over how the filters are applied to the Arel relation.
This example shows how you can implement different approaches for different filters.
def self.apply_filter(records, filter, value, options) case filter when :visibility records.where('users.publicly_visible = ?', value == :public) when :last_name, :first_name, :name if value.is_a?(Array) value.each do |val| records = records.where(_model_class.arel_table[filter].matches(val)) end return records else records.where(_model_class.arel_table[filter].matches(value)) end else return super(records, filter, value) end endFinally if you have more complex requirements for finding you can override the find and find_by_key methods on the resource class.
Here's an example that defers the find operation to a current_user set on the context option:
class AuthorResource < JSONAPI::Resource attribute :name model_name 'Person' has_many :posts filter :name def self.find(filters, options = {}) context = options[:context] authors = context.current_user.find_authors(filters) return authors.map do |author| self.new(author) end end endPagination is performed using a paginator, which is a class responsible for parsing the page request parameters and applying the pagination logic to the results.
JSONAPI::Resource supports several pagination methods by default, and allows you to implement a custom system if the defaults do not meet your needs.
The paged paginator returns results based on pages of a fixed size. Valid page parameters are number and size. If number is omitted the first page is returned. If size is omitted the default_page_size from the configuration settings is used.
The offset paginator returns results based on an offset from the beginning of the resultset. Valid page parameters are offset and limit. If offset is omitted a value of 0 will be used. If limit is omitted the default_page_size from the configuration settings is used.
Custom paginators can be used. These should derive from Paginator. The apply method takes a relation and order_options and is expected to return a relation. The initialize method receives the parameters from the page request parameters. It is up to the paginator author to parse and validate these parameters.
For example, here is a very simple single record at a time paginator:
class SingleRecordPaginator < JSONAPI::Paginator def initialize(params) # param parsing and validation here @page = params.to_i end def apply(relation, order_options) relation.offset(@page).limit(1) end endThe default paginator, which will be used for all resources, is set using JSONAPI.configure. For example, in your config/initializers/jsonapi_resources.rb:
JSONAPI.configure do |config| # built in paginators are :none, :offset, :cursor, :paged config.default_paginator = :offset config.default_page_size = 10 config.maximum_page_size = 20 endIf no default_paginator is configured, pagination will be disabled by default.
Paginators can also be set at the resource-level, which will override the default setting. This is done using the paginator method:
class BookResource < JSONAPI::Resource attribute :title attribute :isbn paginator :offset endTo disable pagination in a resource, specify :none for paginator.
ActiveSupport::Callbacks is used to provide callback functionality, so the behavior is very similar to what you may be used to from ActiveRecord.
For example, you might use a callback to perform authorization on your resource before an action.
class BaseResource < JSONAPI::Resource before_create :authorize_create def authorize_create # ... end endThe types of supported callbacks are:
beforeafteraround
Callbacks can be defined for the following JSONAPI::Resource events:
:create:update:remove:save:create_has_many_link:replace_has_many_links:create_has_one_link:replace_has_one_link:remove_has_many_link:remove_has_one_link:replace_fields
Callbacks can also be defined for JSONAPI::OperationsProcessor events:
:operations: The set of operations.:operation: Any individual operation.:find_operation: Afind_operation.:show_operation: Ashow_operation.:show_relationship_operation: Ashow_relationship_operation.:show_related_resource_operation: Ashow_related_resource_operation.:show_related_resources_operation: Ashow_related_resources_operation.:create_resource_operation: Acreate_resource_operation.:remove_resource_operation: Aremove_resource_operation.:replace_fields_operation: Areplace_fields_operation.:replace_has_one_relationship_operation: Areplace_has_one_relationship_operation.:create_has_many_relationship_operation: Acreate_has_many_relationship_operation.:replace_has_many_relationship_operation: Areplace_has_many_relationship_operation.:remove_has_many_relationship_operation: Aremove_has_many_relationship_operation.:remove_has_one_relationship_operation: Aremove_has_one_relationship_operation.
The operation callbacks have access to two meta data hashes, @operations_meta and @operation_meta, two links hashes, @operations_links and @operation_links, the full list of @operations, each individual @operation and the @result variables.
Note: this can also be accomplished with the top_level_meta_include_record_count option, and in most cases that will be the better option.
To return the total record count of a find operation in the meta data of a find operation you can create a custom OperationsProcessor. For example:
class CountingActiveRecordOperationsProcessor < ActiveRecordOperationsProcessor after_find_operation do @operation_meta[:total_records] = @operation.record_count end endSet the configuration option operations_processor to use the new CountingActiveRecordOperationsProcessor by specifying the snake cased name of the class (without the OperationsProcessor).
JSONAPI.configure do |config| config.operations_processor = :counting_active_record endThe callback code will be called after each find. It will use the same options as the find operation, without the pagination, to collect the record count. This is stored in the operation_meta, which will be returned in the top level meta element.
There are two ways to implement a controller for your resources. Either derive from ResourceController or import the ActsAsResourceController module.
JSONAPI::Resources provides a class, ResourceController, that can be used as the base class for your controllers. ResourceController supports index, show, create, update, and destroy methods. Just deriving your controller from ResourceController will give you a fully functional controller.
For example:
class PeopleController < JSONAPI::ResourceController endOf course you are free to extend this as needed and override action handlers or other methods.
The context that's used for serialization and resource configuration is set by the controller's context method.
For example:
class ApplicationController < JSONAPI::ResourceController def context {current_user: current_user} end end # Specific resource controllers derive from ApplicationController # and share its context class PeopleController < ApplicationController endJSONAPI::Resources also provides a module, JSONAPI::ActsAsResourceController. You can include this module to mix in all the features of ResourceController into your existing controller class.
For example:
class PostsController < ActionController::Base include JSONAPI::ActsAsResourceController endJSONAPI::Resources supports namespacing of controllers and resources. With namespacing you can version your API.
If you namespace your controller it will require a namespaced resource.
In the following example we have a resource that isn't namespaced, and one the has now been namespaced. There are slight differences between the two resources, as might be seen in a new version of an API:
class PostResource < JSONAPI::Resource attribute :title attribute :body attribute :subject has_one :author, class_name: 'Person' has_one :section has_many :tags, acts_as_set: true has_many :comments, acts_as_set: false def subject @model.title end filters :title, :author, :tags, :comments filter :id end ... module Api module V1 class PostResource < JSONAPI::Resource # V1 replaces the non-namespaced resource # V1 no longer supports tags and now calls author 'writer' attribute :title attribute :body attribute :subject has_one :writer, foreign_key: 'author_id' has_one :section has_many :comments, acts_as_set: false def subject @model.title end filters :writer end class WriterResource < JSONAPI::Resource attributes :name, :email model_name 'Person' has_many :posts filter :name end end endThe following controllers are used:
class PostsController < JSONAPI::ResourceController end module Api module V1 class PostsController < JSONAPI::ResourceController end end endYou will also need to namespace your routes:
Rails.application.routes.draw do jsonapi_resources :posts namespace :api do namespace :v1 do jsonapi_resources :posts end end endWhen a namespaced resource is used, any related resources must also be in the same namespace.
Error codes are provided for each error object returned, based on the error. These errors are:
module JSONAPI VALIDATION_ERROR = 100 INVALID_RESOURCE = 101 FILTER_NOT_ALLOWED = 102 INVALID_FIELD_VALUE = 103 INVALID_FIELD = 104 PARAM_NOT_ALLOWED = 105 PARAM_MISSING = 106 INVALID_FILTER_VALUE = 107 COUNT_MISMATCH = 108 KEY_ORDER_MISMATCH = 109 KEY_NOT_INCLUDED_IN_URL = 110 INVALID_INCLUDE = 112 RELATION_EXISTS = 113 INVALID_SORT_CRITERIA = 114 INVALID_LINKS_OBJECT = 115 TYPE_MISMATCH = 116 INVALID_PAGE_OBJECT = 117 INVALID_PAGE_VALUE = 118 INVALID_FIELD_FORMAT = 119 INVALID_FILTERS_SYNTAX = 120 SAVE_FAILED = 121 FORBIDDEN = 403 RECORD_NOT_FOUND = 404 UNSUPPORTED_MEDIA_TYPE = 415 LOCKED = 423 endThese codes can be customized in your app by creating an initializer to override any or all of the codes.
In addition textual error codes can be returned by setting the configuration option use_text_errors = true. For example:
JSONAPI.configure do |config| config.use_text_errors = :true endThe ResourceSerializer can be used to serialize a resource into JSON API compliant JSON. ResourceSerializer must be initialized with the primary resource type it will be serializing. ResourceSerializer has a serialize_to_hash method that takes a resource instance or array of resource instances to serialize. For example:
post = Post.find(1) JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(PostResource.new(post))This returns results like this:
{ "data": { "type": "posts", "id": "1", "links": { "self": "http://example.com/posts/1" }, "attributes": { "title": "New post", "body": "A body!!!", "subject": "New post" }, "relationships": { "section": { "links": { "self": "http://example.com/posts/1/relationships/section", "related": "http://example.com/posts/1/section" }, "data": null }, "author": { "links": { "self": "http://example.com/posts/1/relationships/author", "related": "http://example.com/posts/1/author" }, "data": { "type": "people", "id": "1" } }, "tags": { "links": { "self": "http://example.com/posts/1/relationships/tags", "related": "http://example.com/posts/1/tags" } }, "comments": { "links": { "self": "http://example.com/posts/1/relationships/comments", "related": "http://example.com/posts/1/comments" } } } } }The serialize_to_hash method also takes some optional parameters:
An array of resources. Nested resources can be specified with dot notation.
Purpose: determines which objects will be side loaded with the source objects in an included section
Example: include: ['comments','author','comments.tags','author.posts']
A hash of resource types and arrays of fields for each resource type.
Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and relationship ids in the links section for a resource. Fields are global for a resource type.
Example: fields: { people: [:email, :comments], posts: [:title, :author], comments: [:body, :post]}
post = Post.find(1) include_resources = ['comments','author','comments.tags','author.posts'] JSONAPI::ResourceSerializer.new(PostResource, include: include_resources, fields: { people: [:email, :comments], posts: [:title, :author], tags: [:name], comments: [:body, :post] } ).serialize_to_hash(PostResource.new(post))Context data can be provided to the serializer, which passes it to each resource as it is inspected.
JR has a couple of helper methods available to assist you with setting up routes.
Like resources in ActionDispatch, jsonapi_resources provides resourceful routes mapping between HTTP verbs and URLs and controller actions. This will also setup mappings for relationship URLs for a resource's relationships. For example:
Rails.application.routes.draw do jsonapi_resources :contacts jsonapi_resources :phone_numbers endgives the following routes
Prefix Verb URI Pattern Controller#Action contact_relationships_phone_numbers GET /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#show_relationship {:relationship=>"phone_numbers"} POST /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#create_relationship {:relationship=>"phone_numbers"} DELETE /contacts/:contact_id/relationships/phone-numbers/:keys(.:format) contacts#destroy_relationship {:relationship=>"phone_numbers"} contact_phone_numbers GET /contacts/:contact_id/phone-numbers(.:format) phone_numbers#get_related_resources {:relationship=>"phone_numbers", :source=>"contacts"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy phone_number_relationships_contact GET /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#show_relationship {:relationship=>"contact"} PUT|PATCH /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#update_relationship {:relationship=>"contact"} DELETE /phone-numbers/:phone_number_id/relationships/contact(.:format) phone_numbers#destroy_relationship {:relationship=>"contact"} phone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) contacts#get_related_resource {:relationship=>"contact", :source=>"phone_numbers"} phone_numbers GET /phone-numbers(.:format) phone_numbers#index POST /phone-numbers(.:format) phone_numbers#create phone_number GET /phone-numbers/:id(.:format) phone_numbers#show PATCH /phone-numbers/:id(.:format) phone_numbers#update PUT /phone-numbers/:id(.:format) phone_numbers#update DELETE /phone-numbers/:id(.:format) phone_numbers#destroy Like jsonapi_resources, but for resources you lookup without an id.
By default nested routes are created for getting related resources and manipulating relationships. You can control the nested routes by passing a block into jsonapi_resources or jsonapi_resource. An empty block will not create any nested routes. For example:
Rails.application.routes.draw do jsonapi_resources :contacts do end endgives routes that are only related to the primary resource, and none for its relationships:
Prefix Verb URI Pattern Controller#Action contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy To manually add in the nested routes you can use the jsonapi_links, jsonapi_related_resources and jsonapi_related_resource inside the block. Or, you can add the default set of nested routes using the jsonapi_relationships method. For example:
Rails.application.routes.draw do jsonapi_resources :contacts do jsonapi_relationships end endYou can add relationship routes in with jsonapi_links, for example:
Rails.application.routes.draw do jsonapi_resources :contacts do jsonapi_links :phone_numbers end endGives the following routes:
contact_relationships_phone_numbers GET /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#show_relationship {:relationship=>"phone_numbers"} POST /contacts/:contact_id/relationships/phone-numbers(.:format) contacts#create_relationship {:relationship=>"phone_numbers"} DELETE /contacts/:contact_id/relationships/phone-numbers/:keys(.:format) contacts#destroy_relationship {:relationship=>"phone_numbers"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy The new routes allow you to show, create and destroy the relationships between resources.
Creates a nested route to GET the related has_many resources. For example:
Rails.application.routes.draw do jsonapi_resources :contacts do jsonapi_related_resources :phone_numbers end endgives the following routes:
Prefix Verb URI Pattern Controller#Action contact_phone_numbers GET /contacts/:contact_id/phone-numbers(.:format) phone_numbers#get_related_resources {:relationship=>"phone_numbers", :source=>"contacts"} contacts GET /contacts(.:format) contacts#index POST /contacts(.:format) contacts#create contact GET /contacts/:id(.:format) contacts#show PATCH /contacts/:id(.:format) contacts#update PUT /contacts/:id(.:format) contacts#update DELETE /contacts/:id(.:format) contacts#destroy A single additional route was created to allow you GET the phone numbers through the contact.
Like jsonapi_related_resources, but for has_one related resources.
Rails.application.routes.draw do jsonapi_resources :phone_numbers do jsonapi_related_resource :contact end endgives the following routes:
Prefix Verb URI Pattern Controller#Action phone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) contacts#get_related_resource {:relationship=>"contact", :source=>"phone_numbers"} phone_numbers GET /phone-numbers(.:format) phone_numbers#index POST /phone-numbers(.:format) phone_numbers#create phone_number GET /phone-numbers/:id(.:format) phone_numbers#show PATCH /phone-numbers/:id(.:format) phone_numbers#update PUT /phone-numbers/:id(.:format) phone_numbers#update DELETE /phone-numbers/:id(.:format) phone_numbers#destroy JR by default uses some simple rules to format an attribute for serialization. Strings and Integers are output to JSON as is, and all other values have .to_s applied to them. This outputs something in all cases, but it is certainly not correct for every situation.
If you want to change the way an attribute is serialized you have a couple of ways. The simplest method is to create a getter method on the resource which overrides the attribute and apply the formatting there. For example:
class PersonResource < JSONAPI::Resource attributes :name, :email attribute :last_login_time def last_login_time @model.last_login_time.in_time_zone(@context[:current_user].time_zone).to_s end endThis is simple to implement for a one off situation, but not for example if you want to apply the same formatting rules to all DateTime fields in your system. Another issue is the attribute on the resource will always return a formatted response, whether you want it or not.
To overcome the above limitations JR uses Value Formatters. Value Formatters allow you to control the way values are handled for an attribute. The format can be set per attribute as it is declared in the resource. For example:
class PersonResource < JSONAPI::Resource attributes :name, :email attribute :last_login_time, format: :date_with_utc_timezone endA Value formatter has a format and an unformat method. Here's the base ValueFormatter and DefaultValueFormatter for reference:
module JSONAPI class ValueFormatter < Formatter class << self def format(raw_value) super(raw_value) end def unformat(value) super(value) end ... end end end class DefaultValueFormatter < JSONAPI::ValueFormatter class << self def format(raw_value) case raw_value when String, Integer return raw_value else return raw_value.to_s end end end endYou can also create your own Value Formatter. Value Formatters must be named with the format name followed by ValueFormatter, i.e. DateWithUTCTimezoneValueFormatter and derive from JSONAPI::ValueFormatter. It is recommended that you create a directory for your formatters, called formatters.
The format method is called by the ResourceSerializer as is serializing a resource. The format method takes the raw_value parameter. raw_value is the value as read from the model.
The unformat method is called when processing the request. Each incoming attribute (except links) are run through the unformat method. The unformat method takes a value, which is the value as it comes in on the request. This allows you process the incoming value to alter its state before it is stored in the model.
Another way to handle formatting is to set a different default value formatter. This will affect all attributes that do not have a format set. You can do this by overriding the default_attribute_options method for a resource (or a base resource for a system wide change).
def default_attribute_options {format: :my_default} endand
class MyDefaultValueFormatter < JSONAPI::ValueFormatter class << self def format(raw_value) case raw_value when String, Integer return raw_value when DateTime return raw_value.in_time_zone('UTC').to_s else return raw_value.to_s end end end endThis way all DateTime values will be formatted to display in the UTC timezone.
By default JR uses dasherized keys as per the JSON API naming recommendations. This can be changed by specifying a different key formatter.
For example, to use camel cased keys with an initial lowercase character (JSON's default) create an initializer and add the following:
JSONAPI.configure do |config| # built in key format options are :underscored_key, :camelized_key and :dasherized_key config.json_key_format = :camelized_key endThis will cause the serializer to use the CamelizedKeyFormatter. You can also create your own KeyFormatter, for example:
class UpperCamelizedKeyFormatter < JSONAPI::KeyFormatter class << self def format(key) super.camelize(:upper) end end endYou would specify this in JSONAPI.configure as :upper_camelized.
JR has a few configuration options. Some have already been mentioned above. To set configuration options create an initializer and add the options you wish to set. All options have defaults, so you only need to set the options that are different. The default options are shown below.
JSONAPI.configure do |config| #:underscored_key, :camelized_key, :dasherized_key, or custom config.json_key_format = :dasherized_key #:underscored_route, :camelized_route, :dasherized_route, or custom config.route_format = :dasherized_route #:basic, :active_record, or custom config.operations_processor = :active_record config.allowed_request_params = [:include, :fields, :format, :controller, :action, :sort, :page] # :none, :offset, :paged, or a custom paginator name config.default_paginator = :none # Output pagination links at top level config.top_level_links_include_pagination = true config.default_page_size = 10 config.maximum_page_size = 20 # Output the record count in top level meta data for find operations config.top_level_meta_include_record_count = false config.top_level_meta_record_count_key = :record_count config.use_text_errors = false # List of classes that should not be rescued by the operations processor. # For example, if you use Pundit for authorization, you might # raise a Pundit::NotAuthorizedError at some point during operations # processing. If you want to use Rails' `rescue_from` macro to # catch this error and render a 403 status code, you should add # the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`. config.exception_class_whitelist = [] # Resource Linkage # Controls the serialization of resource linkage for non compound documents # NOTE: always_include_has_many_linkage_data is not currently implemented config.always_include_has_one_linkage_data = false end- Fork it ( http://github.com/cerebris/jsonapi-resources/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Copyright 2014 Cerebris Corporation. MIT License (see LICENSE for details).
