Ruby on Rails Advent Calendar 2017 19日目の記事です。

RailsでAPI開発するときのJSONレスポンスの生成方法についてまとめてみました。

JSONレスポンスの生成方法について

RailsのHTTPレスポンスをJSONで生成する方法は以下の2つに分類されます

  1. renderメソッドにjson引数を指定する(モデル方式)
  2. templateハンドラでレンダリングする(ビュー方式)

ライブラリでいうとactive_model_serializersは1のモデル方式、jbuilderjbは2のビュー方式になります。

モデル方式はrenderメソッドにjson引数が指定されているとActionController::Renderers#_render_with_renderer_json が呼び出され、指定したオブジェクトのto_jsonメソッドが呼び出されることでシリアライズされています。

ビュー方式はテンプレートハンドラを使ってHTMLテンプレートと同じような感覚でビューファイルを記述していき、返すべきJSON文字列を定義していきます。

今回は、JSONレスポンス生成の代表的なライブラリであるactive_model_serializers, jbuilder, jb について、どういった方法でシリアライズを実現しているかをコードベースで紹介していきます。

ライブラリのバージョンは以下になります。

active_model_serializers 0.10.6
jbuilder 2.7.0
jb 0.4.1

active_model_serializersの実装について

jsonに指定したオブジェクトを事前に定義した {モデル名}Serializer クラスやAdapterクラス等で自動的にラップして、Adapterクラスのto_jsonメソッドによってJSONレスポンスが生成されます。Serializerクラスに返却したい属性を定義することでto_jsonのレスポンスも定義されることになります。

自動的にラップする部分は以下のようにモンキーパッチで実装されており、元のオブジェクトをラップした ActiveModelSerializers::SerializableResource オブジェクトを引数として元のメソッドを呼び出しています。

ちなみにRailsだとActiveSupport::ToJsonWithActiveSupportEncoder をObjectクラスがprependしており、ActiveSupport::ToJsonWithActiveSupportEncoder#to_json がas_jsonを呼び出しているので、as_jsonを定義すればto_jsonを間接的に定義していることになります。active_model_serializersのas_jsonは以下のように実装されています。

jbuilderの実装について

テンプレートハンドラを実装することでビューとしてjsonをレンダリングする仕組みです。テンプレートハンドラの中身はこのようになっています。

template.source にはjbuilderで記述したビューファイルの文字列が入ります。暗黙的に使っていたjson変数はJbuilderTemplateクラスのインスタンスになり、このインスタンス内で属性を管理して最後の#target!メソッドで属性をJSONに展開するような作りになっています。

JbuilderTemplateのインスタンスメソッドにはmethod_missingが定義されており、これを使って任意の名前のメソッド呼び出しによって、指定した名前をキーとしたハッシュを作っています。

API開発でリソース指向なAPIにする場合、リソースが単一のときと複数のときで返す属性値を共通化したいというニーズがあります。jbuilderでもビューの一部を共通化して異なるビューで使いまわすパーシャルレンダリングの機能がありますが、複数件のレコードをレンダリングするときにパフォーマンスの問題が生じます。具体的には以下のような書き方をした場合に件数に応じてパフォーマンスが劣化します。

内部的には配列の数だけ_render_partialが呼び出されることになり、N+1 find_template_pathsが発生してレンダリング速度が遅くなります。

jbuilderではcollectionのオプションをわざわざ削除(options.delete(:collection))してから個別にrenderメソッドを呼び出しているわけですが、これはcollectionの引数を渡してパーシャルレンダリングをするとレンダリング結果を結合(#json)してエスケープする(#html_safe)処理が走ってしまうためです。

例えば {"a": 123}というハッシュが3つ入った配列をレンダリングしたい場合には

というようにjsonが文字列として連結された結果が返ることになります。

挙動的にRailsのコレクションレンダリングはHTML/XMLを前提としているような感じなので、partial!という内部のメソッドを提供することで文字列連結されるのを回避しています。

jbの実装について

jbuilderのfind_template_pathsのN+1問題を解決しつつ、DSLではなくRubyのハッシュを書いて自然に書けるようにしたり、値の設定にmethod_missingを使わないようにしたのがjbになります。

面白いのは ActionView::PartialRenderercollection_with_templateメソッドやcollection_without_templateメソッドで返される配列に対してjoinとhtml_safeを局所的にモンキーパッチすることで文字列を結合した結果ではなく、配列のまま返していることです。これによって最終的にテンプレートハンドラの処理結果はハッシュが返されることになります。

これだけだとレスポンスとしてハッシュのto_sが返されるのでrender_template側もモンキーパッチを行い、そこでJSONにしています。

テンプレートハンドラもとてもシンプルでビューファイルをそのままevalしています。

どれを使えばよいか

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

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

いずれにせよビジネスの要件やフェーズによってこれらの方法を使い分けていくのが良いと思います。責務を切り分けた設計・実装ができていれば、複数のJSON生成方法を共存させて順次移行していくこともできそうです。