Quần Cam

The Ruby world now has another serializer

This post is an opinionated comparison between ActiveModel::Serializer, Rabl, Grape::Entity, and Decoradar.

ActiveModel::Serializer

If you’re using Rails, ActiveModel::Serializer (AM::S) might have been your default choice when picking a serializer for your application. I really love the DSL that AM::S brings.

See the following example.

class PostSerializer < ::ActiveModel::Serializer
  attributes :id, :name

  has_many :comments
end

PostSerializer.new(object).as_json

Anyway, AM::S only works if your object is an ActiveRecord aka ActiveModel . If you want to make AM::S works for your plain old ruby object, it’s another horrible long story. Look at this thread for more information.

Roar

Roar is a speedy Parse and render REST API documents using representers, it’s part of Trailblazer.

require 'roar/decorator'
require 'roar/json'

class SongRepresenter < Roar::Decorator
  include Roar::JSON

  property :title
end

Roar is not limited to ActiveRecord, it can be used with any PORO like Sequel::Model, or even a simple struct.

Grape Entity

Grape Entity describes itself as an API focused facade that sits on top of an object model. Entities in Grape Entity can be used to conditionally include fields, nest other entities, and build ever larger responses, using inheritance.

Grape Entity also provides an DSL to declare exposures (fields you want to expose), and supports options like :if, :unless, and :proc to determine which attribute to expose by conditions.

The only thing I personally don’t like about Grape Entity is that it has too troublesome DSL. Your class would end up in something like the following example if you’re trying to lump so many logics to your entity.

module API
  module Entities
    class Status < Grape::Entity
      format_with(:iso_timestamp) { |dt| dt.iso8601 }

      expose :user_name
      expose :text, documentation: { type: "String", desc: "Status update text." }
      expose :ip, if: { type: :full }
      expose :user_type, :user_id, if: lambda { |status, options| status.user.public? }
      expose :location, merge: true
      expose :contact_info do
        expose :phone
        expose :address, merge: true, using: API::Entities::Address
      end
      expose :digest do |status, options|
        Digest::MD5.hexdigest status.txt
      end
      expose :replies, using: API::Entities::Status, as: :responses
      expose :last_reply, using: API::Entities::Status do |status, options|
        status.replies.last
      end

      with_options(format_with: :iso_timestamp) do
        expose :created_at
        expose :updated_at
      end
    end
  end
end

Decoradar

I would love to have an serializer that:

  • Work for any Plain-Old Ruby Object.
  • Has the simple DSL like ActiveModel::Serializer does.
  • Conditional exposure like Grape::Entity.
  • Lastly, comes with the possibility of unit-testable.

That’s the reason why Decoradar was born.

Work for any Plain-Old Ruby Object and Simple DSL

Decoradar has a really simple DSL, attribute and collection.

  • attribute: for value.
  • collection: for collections.
class PostSerializer
  include Decoradar

  attribute :id, :name, :slug
  collection :comments, serializer: CommentSerializer

  def slug
    Slugify.perform(name)
  end
end

post = Post.new(id: 1, name: "Ruby World needs another object serializer", comments: [])
PostSerializer.new(post).as_json
# => {id: 1, name: "Ruby World needs another object serializer", slug: "ruby-world...-serializer", comments: []}

Since Decoradar is just Ruby (it has literally zero dependency) with zero magic stuff, so it works for any PORO-ish objects.

Conditional exposures

Decoradar allows conditional exposure by providing :include_if and :as option in attribute and collection declaration.

class UserSerializer
  include Decoradar

  attributes :id, :name
  attribute :admin, as: :is_admin, include_if: ->(user) { user.admin? }
end

bob = User.new(id: 1, name: "Bob", admin: true)
UserSerializer.new(bob).as_json
# => {id: 1, name: "Bob", is_admin: true}

alice = User.new(id: 2, name: "Alice", admin: false)
UserSerializer.new(alice).as_json
# => {id: 2, name: "Alice"}

Unit-testable

Ease of test is very important and Decoradar was built with that thinking bearing in mind. Consider this example.

class PostSerializer
  include Decoradar

  attributes :id, :name
end

Wanna unit test it? As easy as 1 + 1 = 2.

mocked = mock(id: 1, name: 2)
expected = {id: 1, name: 2}

assert(expected, PostSerializer.new(mocked).as_json)

Benchmark

Benchmark was done based on benchmark_json_renderer on my Macbook Pro 2.7GHz core i5 8 GB 1867 MHz DDR3.

Decoradar

Calculating -------------------------------------
collection ultra simple 148.000  i/100ms
   collection simple    62.000  i/100ms
  collection complex    13.000  i/100ms
-------------------------------------------------
collection ultra simple 1.659k (± 5.6%) i/s -     16.576k
collection simple       532.225  (±15.8%) i/s -      5.208k
collection complex      123.775  (±10.5%) i/s -      1.235k

Roar

collection ultra simple
                        27.000  i/100ms
   collection simple    29.000  i/100ms
  collection complex    29.000  i/100ms
-------------------------------------------------
collection ultra simple
                        297.589  (± 8.4%) i/s -      2.970k
   collection simple    260.505  (±15.4%) i/s -      2.552k
  collection complex    285.935  (±14.7%) i/s -      2.755k

ActiveRecord::Serializer

collection ultra simple
                        79.000  i/100ms
   collection simple    33.000  i/100ms
  collection complex     7.000  i/100ms
-------------------------------------------------
collection ultra simple
                        897.488  (± 4.8%) i/s -      9.006k
   collection simple    343.539  (± 4.1%) i/s -      3.432k
  collection complex     78.961  (± 5.1%) i/s -    791.000

So generally Decoradar was:

  • 2x faster than AM::S and 7x faster then Roar in ultra simple resources.
  • 1.5x faster than AM::S and 2x faster then Roar in simple resources.
  • ~2x faster than AM::S and 2x slower then Roar in complex resources.

on my computer.

That’s it, try Decoradar out and do give me some feedbacks on how to improve this.


NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.

Bài viết cùng chủ đề

Tôi đã tuột quần với Bundler như thế nào?

Khi làm việc với Ruby và Bundler, ta rất hay thường dùng Bundle.setup và Bundle require để load các thư viện trong project. Vậy có bao giờ bạn bị lỗi tuột quần với nó chưa?

Euruko 2017 Notes

Vừa rồi mình đi Euruko 2017 ở Budapest, một số bài nói cũng khá thú vị nên mình sẽ note lại ở đây.

Bundler Gotcha

A few days ago I encountered a strange behavior of Bundler so this post notes down how my experience with it was.