2020年のRuby/RailsのJSONシリアライザは何を使うべきか問題

よくわからなかったので整理してみる。

候補

tl;dr

基本はjbuilderでいいと思う。 パフォーマンスが気になるならActiveModelSerializersか、Jb。ただ、どちらにせよメンテは心配。

Jbuilder

  • https://github.com/rails/jbuilder
  • HEYのGemfile見る限り、DHHは使っているようだ
  • テンプレート評価が遅いとか、パーシャル使うとpartの評価でN+1問題とか、パフォーマンス関連の課題があったみたいだけどどうなんだろう。コード見る限り変わってないような。
  • 開発は継続されている

サンプルコード

json.extract! @post, :id, :title, :content, :published_at
json.author do
  if @post.anonymous?
    json.null! # or json.nil!
  else
    json.first_name @post.author_first_name
    json.last_name @post.author_last_name
  end
end

ActiveModelSerializers

  • https://github.com/rails-api/active_model_serializers
  • Star: 5000
  • masterは2018年から開発が止まっているけど、 0-10-stable ブランチは年初にRuby2.7のサポートが入っている
  • Starも多し定番だった。メンテの頻度下がってるけど今も使ってる人は多いと思う

サンプルコード

class PostSerializer < ActiveModel::Serializer
  attributes :title, :body

  has_many :comments
  has_one :author
end

リアライザは自動で適用されるのでコントローラは変更不要。

class PostsController < ApplicationController

  def show
    @post = Post.find(params[:id])
    render json: @post
  end
end

jsonapi-rb

サンプルコード

class SerializablePost < JSONAPI::Serializable::Resource
  type 'posts'

  attributes :title, :body

  attribute :date do
    @object.created_at
  end

  belongs_to :author

  has_many :comments do
    data do
      @object.published_comments
    end
  end
end

あんまり使いやすそうな気がしない。

JSON:API Serializer

サンプルコード

class MovieSerializer
  include JSONAPI::Serializer

  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
json_string = MovieSerializer.new(movie).serializable_hash.to_json
{
  "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"
        }
      }
    }
  }
}

Blueprinter

サンプルコード

# app/blueprints/todo_blueprint.rb
class TodoBlueprint < Blueprinter::Base
  identifier :id
    
  view :normal do
    fields :name, :due_at, :completed_at
  end

  view :extended do
    include_view :normal
    fields :description, :created_at, :updated_at
  end
end
# app/controllers/api/todos_controller.rb
module Api
  class TodosController < ApplicationController
    def index
      todos = TodoBlueprint.render Todo.all, view: :normal
      render json: todos
    end

    def show
      todo = TodoBlueprint.render Todo.find(params[:id]), view: :extended
      render json: todo
    end
  end
end
[
  {
    "id":1,
    "completed_at":null,
    "due_at":"2018-03-01 23:09:53 UTC",
    "name":"todo0"
  },
  {
    "id":2,
    "completed_at":null,
    "due_at":"2018-03-02 23:09:53 UTC",
    "name":"todo1"
  },
  ...
]

Jb

  • https://github.com/amatsuda/jb
  • Star: 948
  • わりあい安定してメンテされているが、amatsudaが一人でメンテしている感じ。個人のネームスペースだし
  • jbuilderみたいにビューファイルを書くタイプ
    • なので乗り換えは割とやりやすい

サンプルコード

# app/views/messages/show.json.jb

json = {
  content: format_content(@message.content),
  created_at: @message.created_at,
  updated_at: @message.updated_at,
  author: {
    name: @message.creator.name.familiar,
    email_address: @message.creator.email_address_with_name,
    url: url_for(@message.creator, format: :json)
  }
}

if current_user.admin?
  json[:visitors] = calculate_visitors(@message)
end

json[:comments] = @message.comments.map do |comment|
  {
    content: comment.content,
    created_at: comment.created_at
  }
end

json[:attachments] = @message.attachments.map do |attachment|
  {
    filename: attachment.filename,
    url: url_for(attachment)
  }
end

json

JSON APIどうなのか

https://jsonapi.org/

ページングとかの表現とか、みんなバラバラになるから大統一フォーマットを作ろうというので制定されている。 気持ちはわからないでもないけど、この汎用的な、冗長な表現が天下をとるとも思えないんだよなあ。 見れば見るほど気持ちはわかるんだけど、もっと気軽にやりたいんだという。実は僕が知らないだけで、みんなこれ使ってたりする?

こういうレスポンスになる。

{
  "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"
        }
      }
    }
  }
}