Code Monkey home page Code Monkey logo

fast_jsonapi's Introduction

Fast JSON API — ⚠️ This project is no longer maintained!!!! ⚠️

Build Status

A lightning fast JSON:API serializer for Ruby Objects.

Since this project is no longer maintained, please consider using alternatives or the forked project jsonapi-serializer/jsonapi-serializer!

Performance Comparison

We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least 25 times faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the performance document for any questions related to methodology.

Benchmark times for 250 records

$ rspec
Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms

Table of Contents

Features

  • Declaration syntax similar to Active Model Serializer
  • Support for belongs_to, has_many and has_one
  • Support for compound documents (included)
  • Optimized serialization of compound documents
  • Caching

Installation

Add this line to your application's Gemfile:

gem 'fast_jsonapi'

Execute:

$ bundle install

Usage

Rails Generator

You can use the bundled generator if you are using the library inside of a Rails project:

rails g serializer Movie name year

This will create a new serializer in app/serializers/movie_serializer.rb

Model Definition

class Movie
  attr_accessor :id, :name, :year, :actor_ids, :owner_id, :movie_type_id
end

Serializer Definition

class MovieSerializer
  include FastJsonapi::ObjectSerializer
  set_type :movie  # optional
  set_id :owner_id # optional
  attributes :name, :year
  has_many :actors
  belongs_to :owner, record_type: :user
  belongs_to :movie_type
end

Sample Object

movie = Movie.new
movie.id = 232
movie.name = 'test movie'
movie.actor_ids = [1, 2, 3]
movie.owner_id = 3
movie.movie_type_id = 1
movie

Object Serialization

Return a hash

hash = MovieSerializer.new(movie).serializable_hash

Return Serialized JSON

json_string = MovieSerializer.new(movie).serialized_json

Serialized Output

{
  "data": {
    "id": "3",
    "type": "movie",
    "attributes": {
      "name": "test movie",
      "year": null
    },
    "relationships": {
      "actors": {
        "data": [
          {
            "id": "1",
            "type": "actor"
          },
          {
            "id": "2",
            "type": "actor"
          }
        ]
      },
      "owner": {
        "data": {
          "id": "3",
          "type": "user"
        }
      }
    }
  }
}

Key Transforms

By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform

class MovieSerializer
  include FastJsonapi::ObjectSerializer
  # Available options :camel, :camel_lower, :dash, :underscore(default)
  set_key_transform :camel
end

Here are examples of how these options transform the keys

set_key_transform :camel # "some_key" => "SomeKey"
set_key_transform :camel_lower # "some_key" => "someKey"
set_key_transform :dash # "some_key" => "some-key"
set_key_transform :underscore # "some_key" => "some_key"

Attributes

Attributes are defined in FastJsonapi using the attributes method. This method is also aliased as attribute, which is useful when defining a single attribute.

By default, attributes are read directly from the model property of the same name. In this example, name is expected to be a property of the object being serialized:

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attribute :name
end

Custom attributes that must be serialized but do not exist on the model can be declared using Ruby block syntax:

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year

  attribute :name_with_year do |object|
    "#{object.name} (#{object.year})"
  end
end

The block syntax can also be used to override the property on the object:

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attribute :name do |object|
    "#{object.name} Part 2"
  end
end

Attributes can also use a different name by passing the original method or accessor with a proc shortcut:

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name

  attribute :released_in_year, &:year
end

Links Per Object

Links are defined in FastJsonapi using the link method. By default, links are read directly from the model property of the same name. In this example, public_url is expected to be a property of the object being serialized.

You can configure the method to use on the object for example a link with key self will get set to the value returned by a method called url on the movie object.

You can also use a block to define a url as shown in custom_url. You can access params in these blocks as well as shown in personalized_url

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  link :public_url

  link :self, :url

  link :custom_url do |object|
    "http://movies.com/#{object.name}-(#{object.year})"
  end

  link :personalized_url do |object, params|
    "http://movies.com/#{object.name}-#{params[:user].reference_code}"
  end
end

Links on a Relationship

You can specify relationship links by using the links: option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see related resource links)

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  has_many :actors, links: {
    self: :url,
    related: -> (object) {
      "https://movies.com/#{object.id}/actors"
    }
  }
end

This will create a self reference for the relationship, and a related link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the lazy_load_data option:

  has_many :actors, lazy_load_data: true, links: {
    self: :url,
    related: -> (object) {
      "https://movies.com/#{object.id}/actors"
    }
  }

Meta Per Resource

For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  meta do |movie|
    {
      years_since_release: Date.current.year - movie.year
    }
  end
end

Compound Document

Support for top-level and nested included associations through options[:include].

options = {}
options[:meta] = { total: 2 }
options[:links] = {
  self: '...',
  next: '...',
  prev: '...'
}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new([movie, movie], options).serialized_json

Collection Serialization

options[:meta] = { total: 2 }
options[:links] = {
  self: '...',
  next: '...',
  prev: '...'
}
hash = MovieSerializer.new([movie, movie], options).serializable_hash
json_string = MovieSerializer.new([movie, movie], options).serialized_json

Control Over Collection Serialization

You can use is_collection option to have better control over collection serialization.

If this option is not provided or nil autedetect logic is used to try understand if provided resource is a single object or collection.

Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but cannot guarantee that single vs collection will be always detected properly.

options[:is_collection]

was introduced to be able to have precise control this behavior

  • nil or not provided: will try to autodetect single vs collection (please, see notes above)
  • true will always treat input resource as collection
  • false will always treat input resource as single object

Caching

Requires a cache_key method be defined on model:

class MovieSerializer
  include FastJsonapi::ObjectSerializer
  set_type :movie  # optional
  cache_options enabled: true, cache_length: 12.hours
  attributes :name, :year
end

Params

In some cases, attribute values might require more information than what is available on the record, for example, access privileges or other information related to a current authenticated user. The options[:params] value covers these cases by allowing you to pass in a hash of additional parameters necessary for your use case.

Leveraging the new params is easy, when you define a custom attribute or relationship with a block you opt-in to using params by adding it as a block parameter.

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year
  attribute :can_view_early do |movie, params|
    # in here, params is a hash containing the `:current_user` key
    params[:current_user].is_employee? ? true : false
  end

  belongs_to :primary_agent do |movie, params|
    # in here, params is a hash containing the `:current_user` key
    params[:current_user].is_employee? ? true : false
  end
end

# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, {params: {current_user: current_user}})
serializer.serializable_hash

Custom attributes and relationships that only receive the resource are still possible by defining the block to only receive one argument.

Conditional Attributes

Conditional attributes can be defined by passing a Proc to the if key on the attribute method. Return true if the attribute should be serialized, and false if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year
  attribute :release_year, if: Proc.new { |record|
    # Release year will only be serialized if it's greater than 1990
    record.release_year > 1990
  }

  attribute :director, if: Proc.new { |record, params|
    # The director will be serialized only if the :admin key of params is true
    params && params[:admin] == true
  }
end

# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
serializer.serializable_hash

Conditional Relationships

Conditional relationships can be defined by passing a Proc to the if key. Return true if the relationship should be serialized, and false if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  # Actors will only be serialized if the record has any associated actors
  has_many :actors, if: Proc.new { |record| record.actors.any? }

  # Owner will only be serialized if the :admin key of params is true
  belongs_to :owner, if: Proc.new { |record, params| params && params[:admin] == true }
end

# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
serializer.serializable_hash

Sparse Fieldsets

Attributes and relationships can be selectively returned per record type by using the fields option.

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year
end

serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } })
serializer.serializable_hash

Using helper methods

You can mix-in code from another ruby module into your serializer class to reuse functions across your app.

Since a serializer is evaluated in a the context of a class rather than an instance of a class, you need to make sure that your methods act as class methods when mixed in.

Using ActiveSupport::Concern
module AvatarHelper
  extend ActiveSupport::Concern

  class_methods do
    def avatar_url(user)
      user.image.url
    end
  end
end

class UserSerializer
  include FastJsonapi::ObjectSerializer

  include AvatarHelper # mixes in your helper method as class method

  set_type :user

  attributes :name, :email

  attribute :avatar do |user|
    avatar_url(user)
  end
end
Using Plain Old Ruby
module AvatarHelper
  def avatar_url(user)
    user.image.url
  end
end

class UserSerializer
  include FastJsonapi::ObjectSerializer

  extend AvatarHelper # mixes in your helper method as class method

  set_type :user

  attributes :name, :email

  attribute :avatar do |user|
    avatar_url(user)
  end
end

Customizable Options

Option Purpose Example
set_type Type name of Object set_type :movie
key Key of Object belongs_to :owner, key: :user
set_id ID of Object set_id :owner_id or ```set_id {
cache_options Hash to enable caching and set cache length cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds
id_method_name Set custom method name to get ID of an object (If block is provided for the relationship, id_method_name is invoked on the return value of the block instead of the resource object) has_many :locations, id_method_name: :place_ids
object_method_name Set custom method name to get related objects has_many :locations, object_method_name: :places
record_type Set custom Object Type for a relationship belongs_to :owner, record_type: :user
serializer Set custom Serializer for a relationship has_many :actors, serializer: :custom_actor or has_many :actors, serializer: MyApp::Api::V1::ActorSerializer
polymorphic Allows different record types for a polymorphic association has_many :targets, polymorphic: true
polymorphic Sets custom record types for each object class in a polymorphic association has_many :targets, polymorphic: { Person => :person, Group => :group }

Instrumentation

fast_jsonapi also has builtin Skylight integration. To enable, add the following to an initializer:

require 'fast_jsonapi/instrumentation/skylight'

Skylight relies on ActiveSupport::Notifications to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:

require 'fast_jsonapi/instrumentation'

The two instrumented notifcations are supplied by these two constants:

  • FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION
  • FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION

It is also possible to instrument one method without the other by using one of the following require statements:

require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'

Same goes for the Skylight integration:

require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'

Contributing

Please see contribution check for more details on contributing

Running Tests

We use RSpec for testing. We have unit tests, functional tests and performance tests. To run tests use the following command:

rspec

To run tests without the performance tests (for quicker test runs):

rspec spec --tag ~performance:true

To run tests only performance tests:

rspec spec --tag performance:true

fast_jsonapi's People

Contributors

aledalgrande avatar ankit8898 avatar apneadiving avatar bf4 avatar dillonwelch avatar erichmachado avatar erol avatar grossadamm avatar guilleiguaran avatar hmcfletch avatar jshow avatar kyreeves avatar larissa avatar manojmj92 avatar matzko avatar minsikzzang avatar moofkit avatar nicolasleger avatar nikz avatar oboxodo avatar orhantoy avatar rintaun avatar sakuraineed avatar shishirmk avatar sirdharma avatar sriniwasgr avatar trevorhinesley avatar vovimayhem avatar xiphiasuvella avatar y-yagi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fast_jsonapi's Issues

Merge/Inheritance of Serializers

Is there any possibility to use attribute definitions of one Serializer in another?
Like i have:

class SimpleDataSerializer
attributes :a, :b, :c
end

class FullDataSerializer
attributes :a, :b, :c, :d, :e
end

#Scopes
scope :simple
...
scope full, -> do
merge(x.simple).select(additionalData)
end

for many Objects. so it would be nice to include simple in full Serializer and avoid double writing work. Especially to avoid dopple work when attributes in simple change.

Serializer Inheritance doesn't copy attributes

It's not clear that the following won't work as might be expected:

class UserSerializer
  include FastJsonapi::ObjectSerializer

  attribute :first_name
  attribute :last_name
end
class FullUserSerializer < UserSerializer

  attribute :birthday
  attribute :email
end

Workaround:

class UserSerializer
  include FastJsonapi::ObjectSerializer

  def self.inherited(subclass)
    subclass.attributes_to_serialize ||= {}
    subclass.attributes_to_serialize.merge!(attributes_to_serialize)
  end

  attribute :first_name
  attribute :last_name
end

Benchmarks vs jsonapi-rb?

Jsonapi-rb was written by @beauby, who also used to work on AMS (i did too). It's also way faster than AMS.

I saw the medium blog post, and i think more details about the benchmark woulld be good, too.
For example, in my own benchmarks, with 200 records with 2 included relationships each,. I found that jsonapi-rb is 5 to 6 times faster than AMS. I can share the benchmarrk files with you if it'd help.

I have some doubts about the speed increase mentioned in the medium article, which is why i bring this up.

I could do my own benchmarks, but i'm in the car right now. :)

Thanks for supporting/making-more-known the jsonapi spec. :D

Polymorphic associations and Serializer DSL

After documenting #64, I felt a little odd about the polymorphic option I added to the has_many | belongs_to | has_one methods:

class ProjectSerializer
  include FastJsonapi::ObjectSerializer
  belongs_to :owner, polymorphic: { User => :user, Administrator => :administrator }
end

I think the polymorphic option describes an ORM feature (specially when working with relational databases) rather than an object serialization aspect.

Actually, I think the jsonapi spec doesn't even care if an association is polymorphic or not, as long as the associated object has the type key in it, it's all good. I think this should be reflected in the serializer DSL.

So, how about replacing the polymorphic option with being able to use the record_type option with either a symbol (as it is right now) or the Hash object polymorphic receives now?

class ProjectSerializer
  include FastJsonapi::ObjectSerializer
  belongs_to :owner, record_type: { User => :user, Administrator => :administrator }
end

As a matter of fact, since we're memoizing the associated object => record_type with #64 when the Hash isn't provided (Thanks @christophersansone for the tip), I think we can make pretty much the same (performance wise) to not need to specify a record_type at all:

class ProjectSerializer
  include FastJsonapi::ObjectSerializer
  belongs_to :owner # The auto-detect fallback + memoization added in #64 would kick in
end

(You could still define a Hash for record_type, but that would give just a very marginal performance gain when serializing 1 object, I think)

Your thoughts @shishirmk @christophersansone @AtomGrafiks ?

Customize JSON Target-Structure

Hi there,

First thanks for your this awesome gem. :)

Is it somehow possible to customize the json structure from #serialized_json in order to have a different structure?

{
  "data": {
...
    "relationships": {
    ...
    }
  }
}

So for example I want to get rid of 'data' and would like to rename relationships to a customname and so on.

Serialization structure

Hi,

We works with AMS, and we have many options to customize the serialization output.
Actually the serialization object with fast_jsonapi are imposed.

{
  "data": {
    "id": "232",
    "type": "movie",
    "attributes": {
      "name": "test movie",
      "year": null
    },
   ...
}

So, do you have any ideas if you wants add more options to changes structure of data, example if I want build :

GET /movies/:id

{
   "movie": {
     "id": "232",
     "name": "test movie",
     "year": null
   }
}

GET /movies

{
   "movies": [
    {
     "id": "232",
     "name": "test movie",
     "year": null
    },
    {
     "id": "233",
     "name": "another test movie",
     "year": null
    }
  ],
  "meta": { ... }
}

Thanks

Caching: race condition ttl

Rails.cache.fetch might need a race_condition_ttl option?

Setting :race_condition_ttl is very useful in situations where a cache entry is used very frequently and is under heavy load. If a cache expires and due to heavy load several different processes will try to read data natively and then they all will try to write to cache. To avoid that case the first process to find an expired cache entry will bump the cache expiration time by the value set in :race_condition_ttl. Yes, this process is extending the time for a stale value by another few seconds. Because of extended life of the previous cache, other processes will continue to use slightly stale data for a just a bit longer. In the meantime that first process will go ahead and will write into cache the new value. After that all the processes will start getting the new value. The key is to keep :race_condition_ttl small.

Allow includes to be strings

Having a Series serializer with has_many :episodes

SeriesSerializer.new(series, include: ['episodes'])

Throws an ArgumentError.

I would expect it to work, since includes are often pulled from a controller's params, which are always strings.

Having .to_sym the strings would also be a memory issue in Ruby before 2.2.

jbuilder support

I was wondering how can I use fast_jsonapi with jbuilder. I Probably need new adapter in multijson,
problem is that multijson is fast_jsonapi dependency. It's somehow possible?

Shame

insolent lie

Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms

Oj Runtime Dependency

Would you accept a PR for removing Oj as a dependency? Since this is a C gem, it prevents non-MRI VMs from using this gem. With MultiJson, just having Oj loaded in the host app is enough to make to_json use it.

Different underlying models used in benchmarks skew results

The benchmarks use underlying models that are fundamentally different. The models used by fast_jsonapi in the benchmarks are plain Ruby classes, whereas the models used in the ams parts of the benchmarks inherit from ActiveModelSerializers::Model.

This difference skews the benchmarks significantly in favor of fast_jsonapi, after updating the models for the fast_jsonapi parts of the benchmark to also inherit from ActiveModelSerializers::Model several of the benchmark specs fail:

Failures:

  1) FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 25 records atleast 25 times faster than AMS
     Failure/Error: expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
       expected given block to perform faster than comparison block by at_least 25 times, but performed faster by 14.87 times
     # ./spec/lib/object_serializer_performance_spec.rb:105:in `block (4 levels) in <top (required)>'

  2) FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 250 records atleast 25 times faster than AMS
     Failure/Error: expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
       expected given block to perform faster than comparison block by at_least 25 times, but performed faster by 14.78 times
     # ./spec/lib/object_serializer_performance_spec.rb:105:in `block (4 levels) in <top (required)>'

  3) FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 1000 records atleast 25 times faster than AMS
     Failure/Error: expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
       expected given block to perform faster than comparison block by at_least 25 times, but performed by the same time
     # ./spec/lib/object_serializer_performance_spec.rb:105:in `block (4 levels) in <top (required)>'

Classes used in benchmarks:

AMS benchmarks:

class AMSActor < ActiveModelSerializers::Model
  attr_accessor :id, :name, :email
end

fast_jsonapi benchmarks:

class Actor
  attr_accessor :id, :name, :email
end

Deserialization Support

When working with AMS, I find it very useful to be able to deserialize my incoming Json API requests. After looking through the library, it looks like this is not an option. Am I missing something, or is there a reason that deserialization is left out?

This library looks great! I'm using this on my next project, will just need AMS in tandem to deserialize for now. Thanks!

Is it compatible with a project running Rails 4.2.3, Ruby 2.2.1 and Mongoid 5.1.0

I get the following error on bundle update

Bundler could not find compatible versions for gem "activesupport":
  In snapshot (Gemfile.lock):
    activesupport (= 4.2.3)

  In Gemfile:
    fast_jsonapi was resolved to 1.0.17, which depends on
      activesupport (~> 5.0)

    rails (= 4.2.3) was resolved to 4.2.3, which depends on
      activesupport (= 4.2.3)

[Question] `src` in attributes

Hey,

Thanks for this gem !

I haven't see this yet in documentation but do you plan to add the src when you create an object e.g. :

HTTP/1.1 201 Created
Location: http://example.com/photos/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "photos",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "attributes": {
      "title": "Ember Hamster",
      "src": "http://example.com/images/productivity.png"
    },
    "links": {
      "self": "http://example.com/photos/550e8400-e29b-41d4-a716-446655440000"
    }
  }
}

Same for links e.g.:

 "links": {
    "self": "http://example.com/articles/1"
}

Question: why juwelier

The gemspec of juwelier is a mess. What are the advantages of it and running rake gemspec every time vs the regular Bundler gemspec?

Don't require a method called `cache_key` on the model

Given the following model:

class Foo
  attr_accessor :id, :name
  def initialize(opts = {})
    @id = opts[:id]
    @name = opts[:name]
  end
end

and the following serializer:

module Api
  module V1
    class FooSerializer
      include FastJsonapi::ObjectSerializer
      set_type :foo
      cache_options enabled: true, cache_length: 12.hours
      attributes :id, :name
    end
  end
end

an error is raised when called with:

module Api
  module V1
    class FoosController < ApiController
      def show
        foo = Foo.new(id: 1 , name: 'foo')
        render json: FooSerializer.new(foo).serialized_json, status: :ok
      end
  end
end

#=> undefined method `cache_key' for #<Foo:0x00007f94d880b500>

we need to add documentation that this is required for caching or we need to not require it, and I would prefer the later. If cache_key is not defined we should assume a cache_key of the object name and id (in this case "foo:#{object.id}").

has_one serialization fails for nil association

Attempt to serialize model with has_one association fails if association is nil:

NoMethodError (undefined method 'id' for nil:NilClass)

(fast_jsonapi-1.0.16/lib/extensions/has_one.rb:12)

If associated model exists, it gets serialized correctly.

Example:

class User < ApplicationRecord
  has_one :profile
end
 
class Profile < ApplicationRecord
   belongs_to :user
end

class UserSerializer
  include FastJsonapi::ObjectSerializer
  attributes :id, :name
  has_one :profile
end

UserSerializer.new(User.create(name: "John")).serialized_json
NoMethodError (undefined method 'id' for nil:NilClass)

Allow custom method or scope for includes

I think this suggestion is distinct from #47, but it's similar and could possibly be developed at the same time, if others agree and would find this valuable. Alternatively, if there is already a solution to this that would be even better 😄

Scenario is as follows. This is for boxing/UFC fights where there are two athletes (fighters) in each bout, and many bouts in an event.

Serializers:

class FighterSerializer
  include FastJsonapi::ObjectSerializer
  attributes :first_name, :last_name
  has_many :fighter_bouts
end
class FighterBoutSerializer
  include FastJsonapi::ObjectSerializer
  attributes :date_of_event, :name_of_event, :result
end

Models:

class Fighter < ActiveRecord::Base
  has_many  :fighter_bouts

  def custom_method_with_scoping_and_eager_loading
    fighter_bouts.includes(<nested relationships>).where(<scoping of records>).order(<detailed ordering>)
  end
end
class FighterBout < ActiveRecord::Base
  belongs_to :fighter
  belongs_to :bout
end
class Bout < ActiveRecord::Base
  has_many    :fighter_bouts
  belongs_to  :event
  belongs_to  :weight_class
end

What I would like to be able to do is have an endpoint for a Fighter which is a compound document with included fighter_bouts. Such as:

fighter = Fighter.find(1)
opts = {include: [:fighter_bouts]}
payload = FighterSerializer.new(fighter, opts).serialized_json
render json: payload

However, I want to use the custom method defined on the Fighter class, because it will eager load everything needed from a series of nested associations. Data from these nested associations will be "flattened" and presented as attributes on the included fighter_bouts. For example, the event name will be returned as an attribute of a fighter_bout, even though in actuality this value is 2 associations away in the database.

Therefore I was thinking it would be very useful to be able to either pass an additional method name or value along with the options hash being given to the serializer. Or, allow for a custom-named "association method" in the serializer. Extremely roughly conceptualized as:

opts = {include: [:fighter_bouts_custom]}
payload = FighterSerializer.new(fighter, opts).serialized_json
class FighterSerializer
  include FastJsonapi::ObjectSerializer
  attributes :first_name, :last_name
  has_many :fighter_bouts

  def fighter_bouts_custom
    <reference to custom method on the Fighter class>
  end
end

Thanks for reading!

Benchmarks are misleading. I get only 2.5x AMS, and only 1.05x jsonapi-rb

I just did my own benchmarks, and I'm suuuuper open to others checking my work.
But this is what I got (using default settings):

1 User, 2 Posts, 2 Comments per Post. equiv: GET /api/users/:id?include=posts

iterations per second
Comparison:
  fast_jsonapi eager:      547.9 i/s
  jsonapi-rb   eager:      521.4 i/s - 1.05x  (± 0.01) slower
  ams          eager:      219.0 i/s - 2.50x  (± 0.02) slower
                   with 95.0% confidence
memory allocation
Comparison:
  fast_jsonapi eager:      96234 allocated
  jsonapi-rb   eager:     109874 allocated - 1.14x more
  ams          eager:     249662 allocated - 2.59x more

that memory allocation is pretty good though. :-D

Here is the repo where I made the benchmark:
https://github.com/NullVoxPopuli/ruby-jsonapi-benchmarks

Something that needs to be benchmarked is nested includes performance...
AMS and jsonapi-rb both support nested includes. (and this is where jsonapi-rb beats the pants of AMS). :-)

For example:
1 User, 20 Posts, 20 Comments per Post - including 'posts.comments'

Comparison:
  jsonapi-rb   eager:       22.6 i/s
  ams          eager:        4.6 i/s - 4.91x  (± 0.04) slower
                   with 95.0% confidence

Comparison:
  jsonapi-rb   eager:    3979876 allocated
  ams          eager:   12287098 allocated - 3.09x more

Proper separation between the serializer class vs. the serializer instance

This issue is an extraction of the conversations in #49 and #67.

Right now, it is difficult to differentiate between what functionality belongs on the class and what belongs on the instance. Currently they both kinda act as singletons. It poses potential problems, currently and most notably when trying to implement AMS-style custom attribute methods, e.g.:

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year, :name_with_year

  def name_with_year
    "#{object.name} (#{object.year})"
  end
end

In what context is name_with_year executed? What is its scope? How is the object property available within this method? And how do we avoid the performance pitfalls of AMS, that instantiates new serializers for every object being serialized?

I believe an ideal solution would be the following:

  • The serializer class is responsible for containing the definition of the serializer: attributes, relationships, key transformation, etc. Basically, anything built into the serializer DSL would be stored at the class.

  • The serializer instance is responsible for serializing a single object at one time. Its job is to take an object as an input and output the object's serialized representation. This way, additional methods defined on the serializer (custom attributes, custom relationships, etc.) will naturally have an object within the context of the serialization process.

  • The serializer class will have methods to serialize one object or a collection of objects. The class will then create the necessary instance(s). In most cases, developers will be calling class methods to perform the serialization, and they will not be instantiating serializers themselves.

  • We can achieve performance levels comparable to today by reusing the serializer instances within the serialization process, as opposed to generating new instances for every object. Prior to each iteration, we simply set the new object and clear any instance variables that custom methods may have set. From the developer's point of view, it will feel like the serializer is new each time, but without the performance cost.

Examples:

class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year, :name_with_year

  def name_with_year
    "#{object.name} (#{object.year})"
  end
end


MovieSerializer.to_hash(movie, options)
MovieSerializer.to_hash([ movie1, movie2 ], options)

MovieSerializer.to_json(movie, options)
MovieSerializer.to_json([ movie1, movie2 ], options)

Making some decisions along these lines will help set us up for success as we continue to build support for the entire JSON:API 1.0 spec.

How to integrate with DynamoDB using aws-record

As mentioned in title, how can I integrate this with DynamoDB using aws-record gem?

For example, I have this Project model

class Project
  include Aws::Record
  include ActiveModel::Validations

  string_attr :id, hash_key: true
  string_attr :state
  string_attr :title
  string_attr :description
  datetime_attr :start_date
  string_attr :status
  string_attr :company
  string_attr :created_by
end

and then I have this Company model

class Company
  include Aws::Record
  include ActiveModel::Validations

  string_attr :id, hash_key: true
  string_attr :name
end

and then I have the serializer for Project

class ProjectSerializer
  include FastJsonapi::ObjectSerializer

  set_type :project
  attributes :id, :state, :title,
             :description, :start_date, :status,
             :company, :created_by

  has_one :company
end

it return me error as follow:

{"message":"undefined method `company_id' for #\u003cProject:0x00007fbbeac09dd0\u003e\nDid you mean?  company\n               company=\n               company_was"}

I have no idea how to fix this. Please help.

Custom attributes

Will it be possible to add custom attributes to serializers that will delegate to a function within the serializer class. E.g.:

attributes :fullname

def fullname
    "#{object.firstname} #{object.lastname}"
end

Are you guys willing to accept a pull request implementing such a feature?

Include nested of nested association

It would be great to allow to include nested of nested associations... example:

class Post < ApplicationRecord
  has_many :post_tags, dependent: :destroy
  has_many :tags, through: :post_tags
  belongs_to :author, touch: true
end

class Author < ApplicationRecord
  has_many :posts, dependent: :nullify
  has_one :profile, dependent: :destroy
end

class Profile < ApplicationRecord
  belongs_to :author, touch: true
end

In Post serializer, to include author's profile something like:
PostSerializer.new @post, {include: [:author, :'author.profile', :tags]}
could be very useful.

Make `belongs_to` an alias of `has_one`

Looking at the code, there's no real difference for an association being belongs_to and has_one. Instead, is a source for problems (two places with the same code are two places you can forget to place a fix.. have you noticed the default serializer is obtained differently?). Both do the same thing, and both cause the same consequences in the serialization core.

Also, the jsonapi doesn't care about the direction of a 1-to-1 association.

How about:

  • Removing the belongs_to method in object_serializer.rb
  • Adding belongs_to as an alias of has_one, so it is backwards-compatible.

Skylight Integration Suggestions

As one of the lead developers on Skylight it's awesome to see you integrating it!

This is probably a bit counter to our own self-interest, but I think it would probably make a bit more sense if, instead of using Skylight::Helpers you just use ActiveSupport::Notifications. You could then set up a Skylight::Normalizer to consume the events if Skylight was present or other libraries could use them as well. We haven't really flushed out a public API for adding Normalizers so this is definitely something I'd be happy to discuss with you if you're interested.

Error with namespaced relations with inclusions

An error is perform when includes an associations has_one

class Cluster::LocationSerializer 
  include FastJsonapi::ObjectSerializer
  attributes :begin_at, :end_at

  has_one :workspace, serializer: Cluster::WorkspaceSerializer
  has_one :user, serializer: Cluster::UserSerializer
end
class Cluster::UserSerializer 
  include FastJsonapi::ObjectSerializer
  attributes :uid, :login, :nickname, :avatar_url
end
class Cluster::WorkspaceSerializer 
  include FastJsonapi::ObjectSerializer
  attributes :identifier, :campus_id
end

call:

active_locations = Location.active.includes(:workspace, :user).references(:workspace, :user)
Cluster::LocationSerializer.new(active_locations, include: [:workspace, :user]).serializable_hash

Error :

NameError - uninitialized constant Cluster::Cluster

relationship type isn't plural when the resource type is.

example:

"fast_jsonapi"
{
        :data => {
                   :id => "1",
                 :type => :users,
           :attributes => {
            :first_name => "Diana",
             :last_name => "Prince",
              :birthday => -0982-02-05 03:36:03 UTC,
            :created_at => 2018-02-14 02:51:25 UTC,
            :updated_at => 2018-02-14 02:51:25 UTC
        },
        :relationships => {
            :posts => {
                :data => [
                    [0] {
                          :id => "1",
                        :type => :post
                    },
                    [1] {
                          :id => "2",
                        :type => :post
                    }
                ]
            }
        }
    },
    :included => [
        [0] {
                       :id => "1",
                     :type => :posts,
               :attributes => {
                     :title => "Some Post",
                      :body => "awesome content",
                :created_at => 2018-02-14 02:51:25 UTC,
                :updated_at => 2018-02-14 02:51:25 UTC
            },
            :relationships => {
                    :user => {
                    :data => {
                          :id => "1",
                        :type => :user
                    }
                },
                :comments => {
                    :data => [
                        [0] {
                              :id => "1",
                            :type => :comment
                        },
                        [1] {
                              :id => "2",
                            :type => :comment
                        }
                    ]
                }
            }
        },
        [1] {
                       :id => "2",
                     :type => :posts,
               :attributes => {
                     :title => "Some Post",
                      :body => "awesome content",
                :created_at => 2018-02-14 02:51:25 UTC,
                :updated_at => 2018-02-14 02:51:25 UTC
            },
            :relationships => {
                    :user => {
                    :data => {
                          :id => "1",
                        :type => :user
                    }
                },
                :comments => {
                    :data => [
                        [0] {
                              :id => "3",
                            :type => :comment
                        },
                        [1] {
                              :id => "4",
                            :type => :comment
                        }
                    ]
                }
            }
        }
    ]
}

I'm working on a reproduction repo. will post results soon.

Unclear if :relationships can be hydrated

Hi!

Dunno if this is more of a concern of http://jsonapi.org/, but:

it is unclear whether the items contained in :relationships can/should have richer keys than just id and type.

"relationships":{"foos":{"data":[{"id":"2","type":"foo"}]},"bars":{"data":[{"id":"8","type":"bar"}]}}

When one sees this generated json, may wonder "can I have more fields there?"

I am aware that there's included, but ideally one would avoid generating bloated representations. Particularly for a performance-sensitive lib :)

Cheers - Victor

After OJ is enabled for AMS, the speed test randomly failed

According to this PR #36, OJ is enabled for AMS

Now causing the benchmark spec randomly failed, looks like the speed factor is not quite consistent since it's random. I might look into this later anyway.

  1) FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x should serialize 1 records atleast 25 times faster than AMS
     Failure/Error: expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
       expected given block to perform faster than comparison block by at_least 25 times, but performed faster by 22.81 times
     # ./spec/lib/object_serializer_performance_spec.rb:77:in `block (4 levels) in <top (required)>'

  2) FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 25 records atleast 25 times faster than AMS
     Failure/Error: expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
       expected given block to perform faster than comparison block by at_least 25 times, but performed faster by 19.94 times
     # ./spec/lib/object_serializer_performance_spec.rb:102:in `block (4 levels) in <top (required)>'

  3) FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 250 records atleast 25 times faster than AMS
     Failure/Error: expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
       expected given block to perform faster than comparison block by at_least 25 times, but performed faster by 19.44 times
     # ./spec/lib/object_serializer_performance_spec.rb:102:in `block (4 levels) in <top (required)>'

Finished in 25.31 seconds (files took 0.74305 seconds to load)
49 examples, 3 failures

rspec './spec/lib/object_serializer_performance_spec.rb[1:2:1]' # FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x should serialize 1 records atleast 25 times faster than AMS
rspec './spec/lib/object_serializer_performance_spec.rb[1:3:2]' # FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 25 records atleast 25 times faster than AMS
rspec './spec/lib/object_serializer_performance_spec.rb[1:3:3]' # FastJsonapi::ObjectSerializer when comparing with AMS 0.10.x and with includes and meta should serialize 250 records atleast 25 times faster than AMS

No support for 'links' key, which optionally contains 'self', 'next', 'last' and 'related'

Part of the JSON API is to support links but it doesn't appear like there's any way to achieve this right now.

Sample output of this:

{
  "links": {
    "self": "http://example.com/articles",
    "next": "http://example.com/articles?page[offset]=2",
    "last": "http://example.com/articles?page[offset]=10"
  },
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!"
    }
  }]
}

How to override the id attribute ?

According to the JSON API specifications there must be an id field. But I do not want to expose the id column of my DB and I have a friendly_id column (which is unique as well).

How do I instruct fast_jsonapi to use my friendly_id column instead ?

Selecting attributes in associations

Hello!
Prompt, I can not understand, whether there is a possibility to select necessary attributes for the specified association?

class MovieSerializer
  include FastJsonapi::ObjectSerializer
  set_type :movie
  attributes :name
  belongs_to :owner, attributes: [:name]
end
{
  "data": {
    "id": "232",
    "type": "movie",
    "attributes": {
      "name": "test movie"
    },
    "relationships": {
      "owner": {
        "data": {
          "id": "3",
          "type": "user",
          "name": "Owner name example"
        }
      }
    }
  }
}

Is it possible to do this?

Uninitialized Constant on Namespaced Controller

My serializer

module Api
  module V4
    class EmployeeSerializer
      include FastJsonapi::ObjectSerializer
      ...
    end
  end
end

My Controller

module Api
  module V4
    class EmployeesController < ApiController
      ...
      def index
        ...
        ...
        render json: Api::V4::EmployeeSerializer.new(employees), status: :ok
      end
    end
  end
end

got this:

uninitialized constant Api::V4::EmployeeSerializer::FastJsonapi

but it's working well if calling it from console.

was looked at this: #16
but it's different. CMIIW

Question on benchmark results

I'm kind of courious where exactly or what makes fast_jsonapi 25x faster than AMS. Could you provide me some information on this?

It is clear that AMS probably generates some overhead due to a variety of features such as
custom attributes, adapters, conditionals and others. Eventhough the benchmark tests are only using the non-default adapter. E.g.: Only by removing this line of code and using the default adapter the execution times of AWS are dropping almost by 50%

ActiveModel::Serializer.config.adapter = :json_api

Serializer Records Time
AMS serializer 1 1.25 ms
Fast serializer 1 0.14 ms

Serializer Records Time
AMS serializer 25 7.98 ms
Fast serializer 25 0.57 ms

Serializer Records Time
AMS serializer 250 73.46 ms
Fast serializer 250 2.64 ms

Serializer Records Time
AMS serializer 1000 322.16 ms
Fast serializer 1000 15.85 ms

Serializer Records Time
AMS serializer 1 0.55 ms
Fast serializer 1 0.13 ms

Serializer Records Time
AMS serializer 25 5.49 ms
Fast serializer 25 0.55 ms

Serializer Records Time
AMS serializer 250 63.45 ms
Fast serializer 250 4.77 ms

Serializer Records Time
AMS serializer 1000 282.82 ms
Fast serializer 1000 32.25 ms

Don't require id

Currently, if a class that is being serialized does not have an attribute id an error undefined method 'id' is thrown. IMO we should not have an opinion about what attributes are in the class at all. This will allow for non-ActiveRecord classes to also be serialized. At the very least, however, we should document that this is a required field.

Support for CamelCase attributes

We're looking at json serializers specifically for serializing a PORO.

One of the core requirements for serialization is that the snake_cased attribute keys be transformed into CamelCased ones to support the json standards being followed by our organization and also some others like Google, w3resource, schema.org, etc.

Will such a request be considered for this gem? It can be a special configuration item. If yes, I am open to putting a tech design of sorts

has_many relationship serialization

Hello everyone!

I recently started using fast_jsonapi in my rails API and I have to say that the performance improvement is amazing!

I just have a suggestion to make. At the moment, in order to serialise "has_many" relationships, the gem is looking for relationship_model_name_ids and queries on that. With active record serializers, it would be enough to have a method to fetch the relationship data, named after the rails convention.

e.g.

Actor has_many :movies could have a method movies and the relationship data would be serialised.

I mention this because I have some models that are Elasticsearch models and I was not storing the relationship ids, instead I had a method to fetch the relationship data ( I was caching the data as well ) and after I switched to fast_jsonapi the relationship was empty! :)

For now, I store the ids in the mode, but I think It's an easy addition for the gem, if it makes sense to you as well.

Possibly skewing of benchmark results

I noticed 2 issues with how benchmarks are set up.

  1. build_ams_movies builds owner, movie_type and actor objects while build_movies only sets up ids and builds objects just in time when serializing the movies array (and not building owner object at all). It looks like AMS serialization will use more memory and trigger more GC (be slower) because of the way it is set up. Both test setups should initialize same amout of objects/object types
  2. AMS is not set up to use OJ even though it supports it. comparing performance of serializable_hash of both gems or using OJ with both gems should be fairer comparison.

could these 2 issue cause some problems with "fairness" of the benchmark?

dynamic keys

It's possible to serialize object/hash with dynamic keys?

association and polymorphic

Hey, a brief example :

class User < ApplicationRecord
  has_many  :reports, as: :reportable, inverse_of: :reportable
end

class Report < ApplicationRecord
  belongs_to :reportable, polymorphic: true
end

class Group < ApplicationRecord
  has_many  :reports, as: :reportable, inverse_of: :reportable
end

serializer :

class ReportSerializer 
  include FastJsonapi::ObjectSerializer

  has_one :reportable
end

output : (the type of data not relative to reportable_type)

:relationships=> {
  :reportable=> {
    :data=> {
      :id=>"bd729369-6ece-454c-8269-437d1e2ed474", 
      :type=>:reportable
    }
  }
}

type is reportable, not user or group and Fast_JSONAPI fetch a ReportableSerializer not UserSerializer ou GroupSerializer

Implementation of polymorphic association is a great feature 👍

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.