├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── all.yml ├── ams.rb ├── data.rb ├── jsonapi-serializable.rb ├── jsonapi-serializer.rb ├── jsonapi-serializers.rb └── readme.md /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | benchmarks: 7 | runs-on: ubuntu-18.04 8 | strategy: 9 | matrix: 10 | ruby: [2.7] 11 | 12 | steps: 13 | - uses: actions/checkout@master 14 | 15 | - name: Sets up the Ruby version 16 | uses: actions/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | 20 | - name: Sets up the environment 21 | run: | 22 | gem install -q bundler 23 | bundle install 24 | 25 | - name: Runs the benchmarks 26 | run: benchmark-driver ./all.yml --bundler 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Libraries 4 | gem 'jsonapi-serializer', github: 'jsonapi-serializer/jsonapi-serializer' 5 | gem 'active_model_serializers' 6 | gem 'jsonapi-serializable' 7 | gem 'jsonapi-serializers' 8 | 9 | gem 'benchmark_driver' 10 | gem 'ffaker' 11 | 12 | # Sugar 13 | gem 'attr_extras' 14 | -------------------------------------------------------------------------------- /all.yml: -------------------------------------------------------------------------------- 1 | prelude: 'require File.expand_path("data.rb", ENV["PWD"])' 2 | benchmark: 3 | 🐌...JSONAPI::Serializer: 'require File.expand_path("jsonapi-serializer.rb", ENV["PWD"]); run!' 4 | 🐌...JSONAPI::Serializer(many): 'require File.expand_path("jsonapi-serializer.rb", ENV["PWD"]); run_many!' 5 | 🐌...JSONAPI::Serializer(include): 'require File.expand_path("jsonapi-serializer.rb", ENV["PWD"]); run_include_all!' 6 | 🐌...JSONAPI::Serializer(deep): 'require File.expand_path("jsonapi-serializer.rb", ENV["PWD"]); run_include_deep!' 7 | active_model_serializers: 'require File.expand_path("ams.rb", ENV["PWD"]); run!' 8 | active_model_serializers(many): 'require File.expand_path("ams.rb", ENV["PWD"]); run_many!' 9 | active_model_serializers(include): 'require File.expand_path("ams.rb", ENV["PWD"]); run_include_all!' 10 | active_model_serializers(deep): 'require File.expand_path("ams.rb", ENV["PWD"]); run_include_deep!' 11 | jsonapi-serializable: 'require File.expand_path("jsonapi-serializable.rb", ENV["PWD"]); run!' 12 | jsonapi-serializable(many): 'require File.expand_path("jsonapi-serializable.rb", ENV["PWD"]); run_many!' 13 | jsonapi-serializable(include): 'require File.expand_path("jsonapi-serializable.rb", ENV["PWD"]); run_include_all!' 14 | jsonapi-serializable(deep): 'require File.expand_path("jsonapi-serializable.rb", ENV["PWD"]); run_include_deep!' 15 | jsonapi-serializers: 'require File.expand_path("jsonapi-serializers.rb", ENV["PWD"]); run!' 16 | jsonapi-serializers(many): 'require File.expand_path("jsonapi-serializers.rb", ENV["PWD"]); run_many!' 17 | jsonapi-serializers(include): 'require File.expand_path("jsonapi-serializers.rb", ENV["PWD"]); run_include_all!' 18 | jsonapi-serializers(deep): 'require File.expand_path("jsonapi-serializers.rb", ENV["PWD"]); run_include_deep!' 19 | -------------------------------------------------------------------------------- /ams.rb: -------------------------------------------------------------------------------- 1 | require 'active_model_serializers' 2 | 3 | BaseModel.class_eval do 4 | alias :read_attribute_for_serialization :send 5 | end 6 | 7 | ActiveModelSerializers.config.adapter = :json_api 8 | 9 | class AuthorSerializer < ActiveModel::Serializer 10 | attributes :id, :first_name, :last_name 11 | has_many :books 12 | end 13 | 14 | class GenreSerializer < ActiveModel::Serializer 15 | attributes :id, :title, :description 16 | has_many :books 17 | end 18 | 19 | class BookSerializer < ActiveModel::Serializer 20 | attributes :id, :title, :description, :published_at 21 | has_many :authors 22 | belongs_to :genre 23 | end 24 | 25 | def run! 26 | ActiveModelSerializers::SerializableResource 27 | .new(DATA.sample).serializable_hash 28 | end 29 | 30 | def run_many! 31 | ActiveModelSerializers::SerializableResource 32 | .new(DATA).serializable_hash 33 | end 34 | 35 | def run_include_all! 36 | ActiveModelSerializers::SerializableResource.new( 37 | DATA, 38 | include: ['authors', 'genre'] 39 | ).serializable_hash 40 | end 41 | 42 | def run_include_deep! 43 | ActiveModelSerializers::SerializableResource.new( 44 | DATA.sample, 45 | include: [ 46 | 'authors', 47 | 'authors.books', 48 | 'authors.books.genre', 49 | 'authors.books.genre.books', 50 | 'authors.books.genre.books.authors', 51 | 'authors.books.genre.books.genre' 52 | ] 53 | ).serializable_hash 54 | end 55 | -------------------------------------------------------------------------------- /data.rb: -------------------------------------------------------------------------------- 1 | require 'attr_extras' 2 | require 'active_support/all' 3 | require 'securerandom' 4 | require 'ffaker' 5 | 6 | class BaseModel 7 | end 8 | 9 | class Author < BaseModel 10 | vattr_initialize :id, :first_name, :last_name, :books, :book_ids 11 | end 12 | class Genre < BaseModel 13 | vattr_initialize :id, :title, :description, :books, :book_ids 14 | end 15 | class Book < BaseModel 16 | vattr_initialize( 17 | :id, 18 | :title, 19 | :description, 20 | :published_at, 21 | :authors, 22 | :author_ids, 23 | :genre, 24 | :genre_id 25 | ) 26 | 27 | def sync 28 | author_ids = authors.map do |a| 29 | a.books << self 30 | a.book_ids << self.id 31 | a.id 32 | end 33 | genre_id = genre.id 34 | genre.books << self 35 | genre.book_ids << self.id 36 | 37 | self 38 | end 39 | end 40 | 41 | authors = (ENV['DATA_SIZE'] || 1000).to_i.times.map do 42 | Author.new( 43 | SecureRandom.uuid, 44 | FFaker::Name.first_name, 45 | FFaker::Name.last_name, 46 | [], 47 | [] 48 | ) 49 | end 50 | 51 | genres = 10.times.map do 52 | Genre.new( 53 | SecureRandom.uuid, 54 | FFaker::Book.genre, 55 | FFaker::Book.description, 56 | [], 57 | [] 58 | ) 59 | end 60 | 61 | DATA = (ENV['DATA_SIZE'] || 1000).to_i.times.map do 62 | Book.new( 63 | SecureRandom.uuid, 64 | FFaker::Book.title, 65 | FFaker::Book.description, 66 | FFaker::Time.datetime, 67 | authors.sample(rand(1..5)), 68 | [], 69 | genres.sample(), 70 | nil 71 | ).sync 72 | end 73 | -------------------------------------------------------------------------------- /jsonapi-serializable.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi/serializable' 2 | 3 | class AuthorSerializer < JSONAPI::Serializable::Resource 4 | type :author 5 | attributes :first_name, :last_name 6 | has_many :books 7 | end 8 | 9 | class GenreSerializer < JSONAPI::Serializable::Resource 10 | type :genre 11 | attributes :title, :description 12 | has_many :books 13 | end 14 | 15 | class BookSerializer < JSONAPI::Serializable::Resource 16 | type :book 17 | attributes :title, :description, :published_at 18 | has_many :authors 19 | belongs_to :genre 20 | end 21 | 22 | def run! 23 | JSONAPI::Serializable::Renderer.new.render( 24 | DATA.sample, 25 | class: { 26 | Author: AuthorSerializer, 27 | Genre: GenreSerializer, 28 | Book: BookSerializer 29 | } 30 | ) 31 | end 32 | 33 | def run_many! 34 | JSONAPI::Serializable::Renderer.new.render( 35 | DATA, 36 | class: { 37 | Author: AuthorSerializer, 38 | Genre: GenreSerializer, 39 | Book: BookSerializer 40 | } 41 | ) 42 | end 43 | 44 | def run_include_all! 45 | JSONAPI::Serializable::Renderer.new.render( 46 | DATA, 47 | class: { 48 | Author: AuthorSerializer, 49 | Genre: GenreSerializer, 50 | Book: BookSerializer 51 | }, 52 | include: ['authors', 'genre'] 53 | ) 54 | end 55 | 56 | def run_include_deep! 57 | JSONAPI::Serializable::Renderer.new.render( 58 | DATA.sample, 59 | class: { 60 | Author: AuthorSerializer, 61 | Genre: GenreSerializer, 62 | Book: BookSerializer 63 | }, 64 | include: [ 65 | 'authors', 66 | 'authors.books', 67 | 'authors.books.genre', 68 | 'authors.books.genre.books', 69 | 'authors.books.genre.books.authors', 70 | 'authors.books.genre.books.genre' 71 | ] 72 | ) 73 | end 74 | -------------------------------------------------------------------------------- /jsonapi-serializer.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi/serializer' 2 | 3 | class AuthorSerializer 4 | include JSONAPI::Serializer 5 | 6 | attributes :first_name, :last_name 7 | has_many :books 8 | end 9 | 10 | class GenreSerializer 11 | include JSONAPI::Serializer 12 | 13 | attributes :title, :description 14 | has_many :books 15 | end 16 | 17 | class BookSerializer 18 | include JSONAPI::Serializer 19 | 20 | attributes :title, :description, :published_at 21 | has_many :authors 22 | belongs_to :genre 23 | end 24 | 25 | def run! 26 | BookSerializer.new(DATA.sample).serializable_hash 27 | end 28 | 29 | def run_many! 30 | BookSerializer.new(DATA, is_collection: true).serializable_hash 31 | end 32 | 33 | def run_include_all! 34 | BookSerializer.new( 35 | DATA, 36 | is_collection: true, 37 | include: ['authors', 'genre'] 38 | ).serializable_hash 39 | end 40 | 41 | def run_include_deep! 42 | BookSerializer.new( 43 | DATA.sample, 44 | include: [ 45 | 'authors', 46 | 'authors.books', 47 | 'authors.books.genre', 48 | 'authors.books.genre.books', 49 | 'authors.books.genre.books.authors', 50 | 'authors.books.genre.books.genre' 51 | ] 52 | ).serializable_hash 53 | end 54 | -------------------------------------------------------------------------------- /jsonapi-serializers.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi-serializers' 2 | 3 | class AuthorSerializer 4 | include JSONAPI::Serializer 5 | 6 | attributes :first_name, :last_name 7 | has_many :books 8 | end 9 | 10 | class GenreSerializer 11 | include JSONAPI::Serializer 12 | 13 | attributes :title, :description 14 | has_many :books 15 | end 16 | 17 | class BookSerializer 18 | include JSONAPI::Serializer 19 | 20 | attributes :title, :description, :published_at 21 | has_many :authors 22 | has_one :genre 23 | end 24 | 25 | def run! 26 | JSONAPI::Serializer.serialize(DATA.sample) 27 | end 28 | 29 | def run_many! 30 | JSONAPI::Serializer.serialize(DATA, is_collection: true) 31 | end 32 | 33 | def run_include_all! 34 | JSONAPI::Serializer.serialize( 35 | DATA, 36 | is_collection: true, 37 | include: ['authors', 'genre'] 38 | ) 39 | end 40 | 41 | def run_include_deep! 42 | JSONAPI::Serializer.serialize( 43 | DATA.sample, 44 | include: [ 45 | 'authors', 46 | 'authors.books', 47 | 'authors.books.genre', 48 | 'authors.books.genre.books', 49 | 'authors.books.genre.books.authors', 50 | 'authors.books.genre.books.genre' 51 | ] 52 | ) 53 | end 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ruby JSON:API serialization performance comparisons 2 | 3 | An automated suite of tests to explore the performance of the JSON:API 4 | serialization implementations in Ruby. 5 | 6 | The frameworks selection requirements: 7 | * maintained (recent releases, activity in PRs or issues) 8 | * have some popularity (send PRs if you still want it here) 9 | * be a library (we're not testing web frameworks here) 10 | 11 | To start the benchmarks, run: 12 | 13 | ``` 14 | docker run --rm -v `pwd`:/app -w /app -it -e RUBYOPT=-W:no-deprecated ruby:2.7-alpine sh -c 'apk add git build-base && bundle && benchmark-driver ./all.yml --bundler' 15 | ``` 16 | --------------------------------------------------------------------------------