Alt JSON API
JSONApi serializer for Ruby objects. Inspired by Fast JSON API.
Features
- Flexible mapping of attributes
- Custom computed attributes
- Custom ID-mapping
- Manual type setting
- Key and type transforms
- Polymorphic associations
- Sparse fieldsets support
- Nested includes (arbitrary depth)
Installation
Add this line to your application's Gemfile:
gem 'oj'
gem 'jsonapi_serializer'
And then execute:
$ bundle
Usage
Record identification
Records in JSONApi are identified by type
and id
. Normally these attributes can be derived implicitly from serializer name and id by default is just object.id
. But you can redefine this behavior by using following DSL-methods:
class MovieSerializer
include JsonapiSerializer::Base
# Without type hook, type of the record will be derived
# from the serializer name. In this case it would be :movie
type :film
# Using id hook, you can define how id would be retrieved.
# By default it will be taken from `id` attribute of the model.
id { |object| object.slug }
# You can also pass a symbol of attribute of the model that represents id
id :slug
end
You can use public method MovieSerializer.new.id_hash(record)
to get identification object (for example): {id: 'matrix', type: :film}
Types and keys transforms
JSON API spec does not define how exactly keys
and types
should be formatted. For example, if you have a model defined in BlockbusterMovie
, it can be represented by blockbuster_movie
, blockbuster-movie
or blockbusterMovie
, or anything else that makes sense for your application. JsonapiSerializer
allows for customization of this behavior, but only globally so far. If you use Ruby on Rails, you can put these settings in config/initializers/jsonapi_serializer.rb
:
# You can pass a symbol matching on of predefined transforms (:underscore, :dasherize or :camelize)
# or implement your own logic, using block.
# The following example will convert all attribute keys into dasherized-form:
JsonapiSerializer.set_key_transform :dasherize
# This will drop all non alphabet characters and upcase everything:
JsonapiSerializer.set_key_transform do |str|
str.to_s.upcase.gsub(/[^A-Z]/, "").to_sym
end
# The same applies to type transform:
JsonapiSerializer.set_type_transform :camelize
# There is also still unresolved debate on how to treat namespaces in json-api.
# For example, you have `Library::AuthorSerializer` and corresponding model.
# Most of serializers would just drop namespace while trying to retrieve the type.
# We can either drop it too, or replace `::` with defined separator:
JsonapiSerializer.set_type_namespace_separator "-_-"
# Or if you want to ignore (drop) it:
JsonapiSerializer.set_type_namespace_separator :ignore
# The default option is "_"
# Bear in mind that only " " (space), "_" and "-" or any combination of these
# are allowed as per json-api spec, but in practice you can use other symbols if you
# make sure to escape them while using in urls.
Attributes configuration
Alt JSON API supports direct mapping of attributes, as well as remapping and custom attributes that are generated by lambda.
class MovieSerializer
include JsonapiSerializer::Base
# attributes accepts names of attributes
# and/or pairs (hash) of attributes of serialized model
# pointing to attributes of target model
attributes :name, release_year: :year
# attribute accepts a block with serializable record as parameter
attribute :rating do |movie|
"%.2g" % (movie.rating / 10.0)
end
# you can call class methods of serializer to split complex calculations
attribute :comlex_attribute do |movie|
do_heavy_calc(movie)
end
def self.do_heavy_calc(movie)
#...
end
end
In this example, serializer will access record.name
and record.year
to fill attributes name
and release_year
respectively. Rating block will convert 95 into 9.5 and make it string.
Relationships configuration
In order to define relationships, you can use belongs_to
and has_many
DSL methods. They accept options serializer
and from
. By default, serializer will try to guess relationship serializer by the name of relation. In case of :director
relation, it would try to use DirectorSerializer
and crash since it's not defined. Use serializer
parameter to set serializer explicitly. Option from
is used to point at the attribute of the model that actually returns the relation object(s) if it's different. You can also supply lambda, if the relation is accessible in some non-trivial way.
class MovieSerializer
include JsonapiSerializer::Base
belongs_to :director, serializer: PersonSerializer
has_many :actors, from: :cast
# or if you want to introduce some logic in building relationships
has_many :male_actors, from: lambda { |movie| movie.cast.where(sex: "m") }
has_many :female_actors, from: lambda { |movie| movie.cast.where(sex: "f") }
end
From the perspective of serializer, there is no distinction between belongs_to
and has_one
relations. Current implementation does not use ActiveRecord's specifics, such as object.{relation}_id
or object.{relation}_ids
to access relation ids, which means you will have to preload these relations to avoid DB-calls during serialization. This is done deliberately for two reasons:
- Identifier of serialized object (
id
) can be remapped to another attribute, for exampleslug
. - This library is intended to be ORM-agnostic, you can easily use it to serialize some graph structures.
Polymorphic models and relationships
There are two kinds of polymorphism that jsonapi_serializer
supports. First is polymorphic model (STI models in ActiveRecord), where most attributes are shared, but children have different types. Ultimately it is still one kind of entity: think of Vehicle
base class inherited by Car
, Truck
and Motorcycle
. Second kind is polymorphic relationship, where one relationship can contain entirely different models. Let's say you have Post
and Product
, and both can have comments, hence from the perspective of individual comment it belongs to Commentable
. Even though Post
and Model
can share some attributes, their serializers will be used mostly along from comments.
These types of serializers share most of the implementation and both rely on resolver
, which is implicitly defined as a lambda, that applies JsonapiSerializer.type_transform
to the record class name.
Polymorphic Models
To create a serializer for STI models:
class VehicleSerializer
include JsonapiSerializer::Polymorphic
attributes :name, :num_of_wheels
end
class CarSerializer < VehicleSerializer
attributes :trunk_volume
end
class TruckSerializer < VehicleSerializer
attributes :bed_size
end
class MotorcycleSerializer < VehicleSerializer
end
In this case common attributes will be inherited from VehicleSerializer
and children will be registered automatically. Optionally you can add a resolver
to the parent:
resolver do |model|
case model
when Motorcycle then :motorcycle
when Truck then :truck
when Car then :car
end
end
But usually you don't need to, the implicit resolver does the same for you. To specify the rules of type transform and how the namespace is treated, read Types and keys transforms
section.
Polymorphic Relationships
With polymorphic relationships we usually have several independent serializers for models that can appear in one relationships. In order to teach polymorphic serializer to use them, we just need to register these classes using polymorphic_for
.
class CommentableSerializer
include JsonapiSerializer::Polymorphic
# Here we register standalone serializers
# as targets for our polymorphic relationship.
# Be aware that in this case attributes will have no effect.
polymorphic_for PostSerializer, ProductSerializer
# You can set up a resolver here as well!
end
class PostSerializer
include JsonapiSerializer::Base
attributes :title, :body
end
class ProductSerializer
include JsonapiSerializer::Base
attributes :sku, :name, :description
end
Then use it exactly the same as regular serializers. But keep in mind that you cannot randomly inherit serializer classes, an attempt to inherit regular serializer's class will cause an error.
Initialization and serialization
Once serializers are defined, you can instantiate them with several options. Currently supported options are: fields
and include
.
fields
must be a hash, where keys represent record types and values are list of attributes and relationships of the corresponding type that will be present in serialized object. If some type is missing, that means all attributes and relationships defined in serializer will be serialized. In case of polymorphic
serializer, you can supply shared fields under polymorphic type. There is a caveat, though: if you define a fieldset for a parent polymorphic class and omit fieldsets for subclasses it will be considered that you did not want any of attributes and relationships defined in subclass to be serialized. It works the same fashion for polymorphic relationships, so if you want only title
from Post
and name
from Product
, you can supply {commentable: ["title", "name"]}
as a fields
parameter for CommentableSerializer
.
fields
must have attributes as seen by API consumer. For example, if you have key_transform
set to :camelize
, then fields will be expected as {"movie" => ["movieTitle", "releaseYear"]}
, you can use symbols or strings, they will be normallized on serializer instantiation.
include
defines an arbitrary depth tree of included relationships in a similar way as ActiveRecord's includes
. Bear in mind that fields
has precedence, which means that if some relationship is missing in fields, it will not be included either.
options = {}
# We're omitting fieldset for ItemSerializer here,
# only attributes/relationships defined in CommentableSerializer
# will be serialized for Item objects
options[:fields] = {
commentable: [:title],
post: [:body]
}
# You can define arbitrary nesting here
options[:include] = [:tags, author: :some_authors_relation]
serializer = CommentableSerializer.new(options)
Then you can just reuse serializer's instance to serialize appropriate datasets, while supplying optional parameters, such as meta object.
serializer.serialazable_hash(movies, meta: meta)
# or
serializer.serialized_json(movies, meta: meta)
Utils
JsonapiSerializer
provides some convenience methods for converting fields
and include
from query parameters into the form accepted by the serializer.
Fields converter
JsonapiSerializer.convert_fields({"articles" => "title,body", "people" => "name"})
# {articles: [:title, :body], people: [:name]}
Include converter
JsonapiSerializer.convert_include("author,comments.author,comments.theme")
# {author: {}, comments: {author: {}, theme: {}}}
end
Performance
By running bin/benchmark
you can launch performance test locally, however numbers are fluctuating widely. The example output is as follows:
Base case
Adapters | 10 hash/json (ms) | 100 hash/json (ms) | 1000 hash/json (ms) | 10000 hash/json (ms) |
---|---|---|---|---|
JsonapiSerializerTest | 0.39 / 1.17 | 1.32 / 1.75 | 11.26 / 16.55 | 118.13 / 179.28 |
FastJsonapiTest | 0.16 / 0.19 | 1.12 / 1.60 | 10.71 / 16.02 | 104.76 / 160.39 |
With includes
Adapters | 10 hash/json (ms) | 100 hash/json (ms) | 1000 hash/json (ms) | 10000 hash/json (ms) |
---|---|---|---|---|
JsonapiSerializerTest | 0.48 / 0.44 | 1.72 / 2.47 | 13.04 / 17.71 | 125.47 / 179.12 |
FastJsonapiTest | 0.27 / 0.26 | 1.84 / 2.11 | 13.64 / 17.85 | 141.91 / 222.25 |
Performance tests do not include any advanced features, such as fieldsets, nested includes or polymorphic serializers, and were mostly intended to make sure that adding these features did not make serializer slower (or at least significantly slower), but there are models prepared to extend these tests. PRs are welcome.
Roadmap
- Removing as many dependencies as possible. Opt-in JSON-library. Possibly removing dependency on
active_support
. - Creating jsonapi_serializer_rails to make rails integration simple
- ...
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jsonapi_serializer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.