Rails における JSON のシリアライズと向き合う 🦜

Rails で JSON を返す API サーバーを開発する際に、
選択肢となるシリアライズ方法をまとめてみました。
それぞれ Pros/Cons や好みがあると思います。
こういう選択肢があるんだよっていうのを提示できればいいかなーと思っています。
みんながどれを使ってるかアンケートとかも取ってみたい👨‍💻
（※記事の最後に載せました）

# スキーマ
サンプルとしてTwitterをモデルにする。
User と Tweet のテーブルを作成する。

```rb
create_table :users do |t|
  t.string :name, :null => false
  t.string :screen_name, :null => false # @で始まるユーザー名
  t.string :description # 自己紹介
  t.integer :friends_count, :null => false # フォロー数
  t.integer :followers_count, :null => false # フォロワー数
  t.datetime :created_at, :null => false
end
```

```rb
create_table :tweets do |t|
  t.references :user, :null => false
  t.bigint :in_reply_to_status_id # リプライの場合、元のツイートのID
  t.string :text, :null => false
  t.string :source # どのクライアントからツイートしたか
  t.integer :retweet_count, :null => false # リツイート数
  t.integer :favorite_count, :null => false # ふぁぼ数
  t.datetime :created_at, :null => false
end
```

関連はユーザーとツイートが `1:N`

# 基本

`render` に `:json` を指定して、オブジェクトを渡す。

```ruby
def pass_hash
  render :json => { name: "yamada" }
end
```

出力は以下のようになる。

```json
{"name":"yamada"}
```

上の例ではHashを渡したが、内部的には `to_json` がコールされるので、
それが実装されているオブジェクトであれば何でも渡せる。

```ruby
def pass_object_implemented_to_json
  hoge = Hoge.new("yamada")
  render :json => hoge
end

class Hoge
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def to_json(options)
    {
      name: "#{@name}さん"
    }.to_json
  end
end

# => {"name":"yamadaさん"}
```

自前で `to_json` を実装するときは引数を1つもらうようにする。
こんなのが渡されてくるので。

```ruby
{:prefixes=>["users"], :template=>"index", :layout=>#<Proc:0x0000561d82dd2cc8@/usr/local/bundle/gems/actionview-6.0.1/lib/action_view/layouts.rb:392>}
```

また、 `to_json` を実装したオブジェクト以外にも、単なる文字列も渡せます。


```ruby
def to_json(options)
  @name
end
# => yamadaさん
```

どんな実装になっているのか覗いてみると、

```ruby:action_controller/metal/renderers.rb
add :json do |json, options|
  json = json.to_json(options) unless json.kind_of?(String)

  if options[:callback].present?
    if media_type.nil? || media_type == Mime[:json]
      self.content_type = Mime[:js]
    end

    "/**/#{options[:callback]}(#{json})"
  else
    self.content_type = Mime[:json] if media_type.nil?
    json
  end
end
```

`kind_of?` で型を見て String じゃなかったら `to_json` を呼んでる。

なのであらかじめJSON文字列化された値を渡しても動くという。

```ruby
def to_json(options)
  "{\"name\":\"#{@name}\"}"
end
```

あまり実用的ではなさそうだけど。

シンプルな方法というか、基本原理はこんな感じで、
あとはJSONの中身の生成/整形をどこでやるかが勘所になってくる。

# ActiveModel::Serialization
ActiveRecordは `ActiveModel::Serialization` を include しているので、そのまま利用できる。
[Active Modelの基礎 1.8 シリアライズ](https://railsguides.jp/active_model_basics.html#%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA)

```ruby:app/controllers/user_controller.rb
class UserController < ApplicationController
  def sample
    user = User.first
    render :json => user
  end
end
```

モデルに特別何も定義していなければ、全てのカラムがそのまま出力される。

```json
{
  "id":1,
  "name":"山田",
  "screen_name":"@yamada",
  "description":"システムエンジニアです",
  "friends_count":100,
  "followers_count":50,
  "created_at":"2019-12-09T10:46:29.000Z"
}
```

モデルに `attributes` を定義することでシリアライズのされ方が変わる。


```ruby:app/models/user.rb
class User < ApplicationRecord
  def attributes
    {'name' => nil, 'screen_name' => nil }
  end 
end
```

```json
{
  "name":"山田",
  "screen_name":"@yamada",
}
```

キーにシンボルが使えなかったり、値をnilにしたり、使い勝手としてはなんか気持ち悪い。

実装自体は ActiveModel のもので、 `serializable_hash` というメソッドが生える。
対応するインスタンス変数やメソッドをいい感じに探してくれるらしい。

```ruby:app/models/user.rb
class User < ApplicationRecord
  # 新しいメソッドを定義する
  def created_date
    created_at.strftime("%Y年%m月%d日")
  end
end
```

```ruby
user.serializable_hash(only: :name)   # => {"name"=>"山田"}
user.serializable_hash(except: :name) # => {"screen_name"=>"@yamada"}
user.serializable_hash(methods: :created_date) # => {"name"=>"山田", "screen_name"=>"yamada", "created_date"=>"2019年12月09日"}
```


# [Jbuilder](https://github.com/rails/jbuilder)

>generate JSON objects with a Builder-style DSL

テンプレートファイルを用意して、独自のDSLで記述する。
Railsに標準で同梱されている。

```ruby:app/controllers/jbuilder_controller.rb
class JbuilderController < ApplicationController
  def sample
    @user = User.first
    render :formats => :json
  end
end
```

または `:handlers` を明示する。

```ruby
render :formats => :json, :handlers => :jbuilder
```


```app/views/jbuilder/sample.json.jbuilder
json.name @user.name
json.screen_name @user.screen_name
json.followers_count "#{@user.followers_count}人"
```

```json
{
  "name":"山田",
  "screen_name":"@yamada",
  "followers_count":"50人"
}
```

コントローラー側でインスタンス変数にデータを格納して、
jbuilderでいじくりまわすので普通にビュー作るときと同じノリで書ける。

とにかく小回りが利く、柔軟、自由度が高いっていうのがたぶんProsで、
複数モデルを取り回すのとか、複雑な分岐とか凝ったことやりたいときに向いているのかなーというのが所管なんだけど、
あまりに好きに書けすぎるので気をつけないとごちゃごちゃになって破綻しそう。

ネストさせたり、関連や条件分岐を足してみる。

```app/views/jbuilder/sample.json.jbuilder
json.name @user.name
json.screen_name @user.screen_name

json.count do
  json.follows "#{@user.friends_count}人"
  json.followers "#{@user.followers_count}人"
end

json.tweets @user.tweets, :id, :text, :created_at

if (Time.now - @user.created_at) / 24 / 60 / 60 < 7
  json.new_user true 
end
```

```json
{
  "name":"山田",
  "screen_name":"@yamada",
  "count":{
    "follows":"100人",
    "followers":"50人"
  },
  "tweets":[
    {
      "id":1,
      "text":"おなかへった",
      "created_at":"2019-12-09T10:46:29.000Z"
    }
  ],
  "new_user":true
}
```

レイヤーとしてはビューなのでビジネスロジック書くのにもちょっと抵抗がある。
そうなると結局間に decorator とか プレゼンター層挟むの？みたいになってしまう。

まあでもこういう書き方が好みの人もいるのかなーという感じ。
DSLも掘ったら奥が深いので、Jbuilder職人みたいな人が爆誕するたぶん。

あとは、jbuilder遅い問題とどう向き合うか。

- [jbuilderで無理矢理N+1 partial renderingを回避する方法](https://blog.freedom-man.com/jbuilder-n1render)
- [jbuilderのpartialを高速化する](https://qiita.com/alpaca_taichou/items/4a1e8da05ba2463bf498)
- [ActiveModel::Serializers vs Jbuilder](https://kirillplatonov.com/2014/11/04/active_model_serializer_vs_jbuilder/)

# [Jb](https://github.com/amatsuda/jb)
Jbuilder 遅い & DSL気持ち悪い問題に挑んでいるライブラリ。
>A simple and fast JSON API template engine for Ruby on Rails
A simpler and faster Jbuilder alternative.

```rb:Gemfile
gem 'jb'
```

Rubyを気持ちよく書けるようになっている。
The Ruby.
Jbuilderの例と同じようにシリアライズしてみる。

```rb:app/views/jb/sample.json.jb
json = {
  name: @user.name,
  screen_name: @user.screen_name,
  count: {
    follows: "#{@user.friends_count}人",
    followers: "#{@user.followers_count}人"
  }
}

json[:tweets] = @user.tweets.map do |tweet|
  {
    id: tweet.id,
    text: tweet.text,
    created_at: tweet.created_at
  }
end

if (Time.now - @user.created_at) / 24 / 60 / 60 < 7
  json[:new_user] = true 
end

json
```

template を render する

```rb:app/controllers/jb_controller.rb
class JbController < ApplicationController
  def sample
    @user = User.first
    render :template => "jb/sample.json.jb"
  end
end
```

Featuresの 
>No ugly builder syntax

に重みがある😂

# [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers)
シリアライズ専用のモデルにJSONの定義を委譲する形のもの。
Jbuilderみたいなテンプレートはなく、Rubyのオブジェクトにマクロで記述していく。


```ruby:Gemfile
gem 'active_model_serializers'
```

```ruby:app/controllers/active_model_serializer_controller.rb
class ActiveModelSerializerController < ApplicationController
  def sample
    user = User.first
    render :json => user
  end
end
```

```ruby:app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :name, :screen_name, :description
end
```


```json
{
  "name":"山田",
  "screen_name":"@yamada",
  "description":"システムエンジニアです"
}
```

シリアライズという目的だけに特化したオブジェクトなので、綺麗に責務が分離できるのが気持ちいい。
JSONの整形に必要なロジックを、モデルから取り除いてここに全部詰め込めばよい。

シリアライズに使用するクラスは `render` の引数で自由に変更できるので、
1つのモデルに対して複数のシリアライズを表現できるのも便利。

```ruby
render :json => user, :serializer => NeoUserSerializer
```

```rb:app/serializers/neo_user_serializer.rb
class NeoUserSerializer < ActiveModel::Serializer
  attribute :neo_name do
    "ネオ#{object.name}"
  end
end
```

```json
{"neo_name":"ネオ山田"}
```

コントローラーからオプションの引数を渡すこともできるし、

```rb
render :json => user, :with_tweets => true
```

それを使って条件付きでシリアライズに含めるかどうかを制御するのはこうやる。

```rb:app/serializers/user_serailizer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :name, :screen_name, :description

  def initialize(object, **option)
    @with_tweets = option[:with_tweets]
    super
  end

  attribute :tweets, if: -> { @with_tweets }

  def tweets
    object.tweets
  end
end

```

```json
{
  "name":"山田",
  "screen_name":"@yamada",
  "description":"システムエンジニアです",
  "tweets":[
    {
      "id":1,
      "user_id":1,
      "in_reply_to_status_id":null,
      "text":"おなかへった",
      "source":"iPhone",
      "retweet_count":0,
      "favorite_count":0,
      "created_at":"2019-12-09T10:46:29.000Z"
    }
  ]
}
```

`has_many`, `belongs_to`, `has_one` がサポートされているので、
関連は以下のようにも表現できる。

```rb:app/serializers/user_serailizer.rb
has_many :tweets
```

この時、子レコードはどうシリアライズされるのかというと、
これまた対応するモデルのシリアライザーを勝手に呼び出してくれる。

なので、1モデル1シリアライザーを定義しておけば、
アプリケーション内で一貫して統一の取れたリソース指向のレスポンスを構築することができる。

テストもしやすい。

そしてPOROにも使うことができる。

よい😇

# [Fast JSON API](https://github.com/Netflix/fast_jsonapi)
Active Model Serializer をベースに、パフォーマンスを改善して高速化させたNetflix社謹製のライブラリ。
ベンチマークで25倍の速さが出たそう。

書き方は ActiveModelSerializers に近いのだが、
シリアライズされたものの体裁がだいぶ異なる。

```rb:app/controllers/fast_json_api_controller.rb
class FastJsonApiController < ApplicationController
  def sample
    user = User.first
    options = { :include => [:tweets] }
    serializer = UserSerializer.new(user, options)
    render :json => serializer.serialized_json
  end
end
```

```rb:app/serializers/user_serializer.rb
class UserSerializer
  include FastJsonapi::ObjectSerializer
  attributes :name, :screen_name
  has_many :tweets
end
```

```rb:app/serializers/tweet_serializer.rb
class TweetSerializer
  include FastJsonapi::ObjectSerializer
  attributes :text
end
```

```json
{
  "data": {
    "id": "1",
    "type": "user",
    "attributes": {
      "name": "山田",
      "screen_name": "@yamada"
    },
    "relationships": {
      "tweets": {
        "data": [
          {
            "id": "1",
            "type": "tweet"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "tweet",
      "attributes": {
        "text": "おなかへった"
      }
    }
  ]
}
```

これは何かと言うと、ライブラリの説明にも書いてある通り、
[JSON:API](https://jsonapi.org/) の仕様に沿ったシリアライズらしい。
>A lightning fast JSON:API serializer for Ruby Objects.

新し目のライブラリなのでまだまだ足りないところはありそうですが、
今後に期待といったところですかね。

# [JSONAPI::Resources](https://github.com/cerebris/jsonapi-resources)
>A resource-focused Rails library for developing JSON:API compliant servers

 
>JSONAPI::Resources, or "JR", provides a framework for developing an API server that complies with the JSON:API specification.

これも JSON:API の仕様に則ったシリアライズを提供するライブラリ？フレームワーク？
現在の最新バージョンは0.10

```ruby:Gemfile
gem 'jsonapi-resources'
```

モデルは ActiveRecord でも他のオブジェクトでもいいらしいが、 
`app/resources` というディレクトリに、 `***_resource.rb` という名前で置く。
`app/resources` なんてディレクトリ初めて見たかもしれない。

```rb:app/resources/user_resource.rb
class UserResource < JSONAPI::Resource
  attributes :name, :screen_name
  has_many :tweets
end
```

```rb:app/resources/tweet_resource.rb
class TweetResource < JSONAPI::Resource
  attributes :text, :source, :retweet_count, :favorite_count
end
```

ルーティングのヘルパーも提供されていて、 routes.rb に以下のように書いてみると、

```rb:config/routes.rb
Rails.application.routes.draw do
  jsonapi_resources :users
  jsonapi_resources :tweets
end
```

こういうルーティングができあがる。

``` 
user_relationships_tweets GET       /users/:user_id/relationships/tweets(.:format)                                           users#show_relationship {:relationship=>"tweets"}
                          POST      /users/:user_id/relationships/tweets(.:format)                                           users#create_relationship {:relationship=>"tweets"}
                          PUT|PATCH /users/:user_id/relationships/tweets(.:format)                                           users#update_relationship {:relationship=>"tweets"}
                          DELETE    /users/:user_id/relationships/tweets(.:format)                                           users#destroy_relationship {:relationship=>"tweets"}
      user_related_tweets GET       /users/:user_id/tweets(.:format)                                                         tweets#index_related_resources {:relationship=>"tweets", :source=>"users"}
                          GET       /users(.:format)                                                                         users#index
                          POST      /users(.:format)                                                                         users#create
                     user GET       /users/:id(.:format)                                                                     users#show
                          PATCH     /users/:id(.:format)                                                                     users#update
                          PUT       /users/:id(.:format)                                                                     users#update
                          DELETE    /users/:id(.:format)                                                                     users#destroy
                   tweets GET       /tweets(.:format)                                                                        tweets#index
                          POST      /tweets(.:format)                                                                        tweets#create
                    tweet GET       /tweets/:id(.:format)                                                                    tweets#show
                          PATCH     /tweets/:id(.:format)                                                                    tweets#update
                          PUT       /tweets/:id(.:format)                                                                    tweets#update
                          DELETE    /tweets/:id(.:format)                                                                    tweets#destroy
```

JSON:API っぽいやつですね。
詳しくはここを => [Routing](https://jsonapi-resources.com/v0.10/guide/routing.html)

次はコントローラー。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  include JSONAPI::ActsAsResourceController
end
```

```rb:app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  include JSONAPI::ActsAsResourceController
end
```

アクション書く必要はありません。
これで試しに `/users` にアクセスしてみます。

**/users**

```json
{
  "data": [
    {
      "id": "1",
      "type": "users",
      "links": {
        "self": "http://localhost:3000/users/1"
      },
      "attributes": {
        "name": "山田",
        "screen-name": "@yamada"
      },
      "relationships": {
        "tweets": {
          "links": {
            "self": "http://localhost:3000/users/1/relationships/tweets",
            "related": "http://localhost:3000/users/1/tweets"
          }
        }
      }
    },
    {
      "id": "2",
      "type": "users",
      "links": {
        "self": "http://localhost:3000/users/2"
      },
      "attributes": {
        "name": "ボブ",
        "screen-name": "@bob"
      },
      "relationships": {
        "tweets": {
          "links": {
            "self": "http://localhost:3000/users/2/relationships/tweets",
            "related": "http://localhost:3000/users/2/tweets"
          }
        }
      }
    },
  ]
}
```

えっ、すげえ。
せっかくなのでリンクをたどる。

**/users/1/relationships/tweets**

```json
{
  "data": [
    {
      "type": "tweets",
      "id": "1"
    }
  ],
  "links": {
    "self": "http://localhost:3000/users/1/relationships/tweets",
    "related": "http://localhost:3000/users/1/tweets"
  }
}
```

**/users/1/tweets**

```json
{
  "data": [
    {
      "id": "1",
      "type": "tweets",
      "links": {
        "self": "http://localhost:3000/tweets/1"
      },
      "attributes": {
        "text": "おなかへった",
        "source": "iPhone",
        "retweet-count": 0,
        "favorite-count": 0
      }
    }
  ]
}
```

はえ〜。便利そうだけど初見ではかなりブラックボックスだな〜。

[Resources](https://jsonapi-resources.com/v0.10/guide/resources.html) とか奥が深そう。
シリアライズだけじゃなくて書き込みもあるわルーティングもやってくれるわ、フレームワークですね。

これ Rails に組み込むのは色んな意味でありなのか... :thinking: 


# 結局どれがいいのか
ただライブラリ紹介しただけでは芸がないので界隈の意見を見てみます。

## [Ruby on RailsのJSON API Responseについて考える。あるいはjbuilderについての個人的違和感とその解](https://note.com/teitei_tk/n/nc6309350648a)

>人によると思いますが、個人的にはjbuilderのDSLには違和感があります。
これは個人差だと思いますが、下記のif文を見てみます。

```ruby
if current_user.admin?
 json.visitors calculate_visitors(@message)
end
```

>current_userがadminであれば、生成されるJSONにvisitorsというattributeが追加されます。
逆に言えば、current_userがadminではない場合は、visitorsというattributteは付きません。

>サーバサイドであればJSON Responseを返すだけで後はクライアント(JavaScript、iOS・Android etc)でいい感じにやってくれ。
という事も出来ますが、userの属性によってattributeがついたり消えたりするのはクライアントのコードが複雑になることに繋がります。

DSL に違和感があるのは同意だが、attributeが付く付かないに関して懸念はない。
OpenAPI 信者なので、値がないのにキーとして含める方がめんどくさいと思っている。
■ 参考：[そのフィールド、nullable にしますか、requiredにしますか](https://qiita.com/sakuraya/items/2d98c352c74ad72e2a9a)

>一つの解として、ActiveModelSerializerというgemがあります。
が、2018/12 現在、ActiveModelSerializerは積極的にメンテナンスはされていません。

確かに。いま現在でも master の最終コミットが 28 Aug 2018 だぞ。
<img width="997" alt=" 2019-12-09 22.36.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/5630a9e6-fdd8-6dd0-8ec6-9fdeda2cb219.png">

すごいみんな使ってるし、メンテしたいなー。

記事の結論としては fast_jsonapi 推しだけど、
JSON:API をどう捉えるかですかねー。

## [Rails API開発におけるJSONレスポンス生成方法と内部実装について](https://blog.freedom-man.com/rails-api-json-implementation)
>jbuilderのfind_template_pathsのN+1問題を解決しつつ、DSLではなくRubyのハッシュを書いて自然に書けるようにしたり、値の設定にmethod_missingを使わないようにしたのがjbになります。

Jbの話。
ベンチマークでは3~30倍くらいの速さが出たらしい。
個人的には Jbuilder よりはこちらを推したいな :thinking: 

速度はさておき、あのDSLが好きかどうかみたいなところもありそう。
潔癖な人は Jb のとにかく Ruby で殴るみたいな感じを嫌いそうだなーとも思ったり。

>リソース指向が強いAPIであれば、active_model_serializersが良さそうです。ViewObject的な立ち回りでテスト可能であることも強みだと思います。

>一方で、リソース指向が強くなければjbuilderやjbが書きやすいです。速度的にはjbが良さそうですが、ビジネス要件としての速度を満たすかどうかという点では大抵のケースにおいて、jbuilderでも問題無いです。コレクションレンダリングのパフォーマンスとテンプレートの共通化の部分がjbuilderを使うかどうかの論点になると思います。

やはりきれいにリソース指向にするなら Active Model Serializers、
複雑なことやるなら JBuilder みたいなテンプレートエンジンなんだろうか

# まとめ
- リソース指向かテンプレートか
- JSON:APIに準拠するかどうか
- Ruby か DSL か
- 速度はどこまで許容できるか
- テスタビリティ
- シリアライズをどの層に位置づけるか

あたりが選定基準かな :thinking:

アンケートやってみるか

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Rails で JSON のシリアライズになに使ってますか</p>&mdash; 青いエンジニア🦋 (@itmono_sakuraya) <a href="https://twitter.com/itmono_sakuraya/status/1204102920297279493?ref_src=twsrc%5Etfw">December 9, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 


他にもあったら教えてください。
以上でした。