Posted at

ActiveModel::Serializersを使ってサクサクjsonを出力しよう

More than 5 years have passed since last update.


ActiveModel::Serializersとは

Railsなどで簡単で素早くjsonを作れるgemです。

ActiveModel::Serializersより、


ActiveModel::Serializers brings convention over configuration to your JSON generation.


実際に試してはないですが、ActiveModelに依存しているだけなのでSinatraなどでも使えるはずです。


なぜActiveModel::Serializersを使うのか

簡単で速いので使ってます。


簡単

Railsではjbuilderを使うことが多いですが、記法が直感的じゃなくて多少憶えにくいかな思ってます。

json.(@message, :content, :author)

=> {content: 'hello', author: 'kakkunpakkun'}

ちょっと変わった文です。

ActiveModel::Serializerならこんな感じです。

class MessageSerializer < ActiveModel::Serializer

attributes :content, :author, :role

def role
object.role.name
end
end

呼び出すのもrenderを使えます

render json: @message

ActiveModel::Serializerを継承して使いたいattributesを指定するのが基本です。

roleのようにattributeをオーバーライドするようなことも出来ます。

普通のRubyのクラスなのでテストも書きやすいです。

実際、HTMLを生成するのにはテンプレートがあるといいと思うんですが、jsonはもっとシンプルだし、テンプレートは必要ないんじゃないかと思っていました。

とはいえ、to_jsonだとモデルをいじらなくてはいけなくなってプレゼンテーション層と密着した感じになってしまいます。

ActiveModel::Serializersはそういうjsonの出力をさくっと簡単に担ってくれます。


速い

partialを使うとjbuilderはものすごく遅くなります。

適当にusersをリストアップする場合で計ってみました。


jbuilderを使った場合


users_controller.rb

def list_with_jbuilder

@users = User.all
render :list_with_jbuilder
end



list_with_jbuilder.jbuilder

json.array! @users.each do |user|

json.partial! 'user', user: user
end


_user.jbuilder

json.id user.id

json.email user.email
json.name user.display_name

だいたいusersのようなコレクションをリストにするときはuserの情報を他のところからも呼び出したいことがあるのでpartial化するのですが、そうするとここではuserを100件出力したところ 280〜350ms ほどかかりました。

userを5件だけに絞ると50〜60msで済むのですが、partialを呼び出す件数が多くなればなるほど遅くなっていきます。

これは恐らくpartialを呼ぶたびに0.1〜0.3msほどかかっているためではないかと思います。

I, [2014-08-31T15:30:38.740573 #65161]  INFO -- : Processing by UsersController#list_with_jbuilder as */*

D, [2014-08-31T15:30:38.748207 #65161] DEBUG -- : User Load (1.8ms) SELECT `users`.* FROM `users`
I, [2014-08-31T15:30:38.755020 #65161] INFO -- : Rendered users/_user.jbuilder (0.2ms)
I, [2014-08-31T15:30:38.757443 #65161] INFO -- : Rendered users/_user.jbuilder (0.2ms)
I, [2014-08-31T15:30:38.759521 #65161] INFO -- : Rendered users/_user.jbuilder (0.2ms)

# ↑これがuserの件数分続く

partial使わなければいいんですが、そうすると変更箇所が増えて維持が大変になります。


ActiveModel::Serializersを使った場合


users_controller.rb

def list_with_serializer

users = User.all
render json: ActiveModel::ArraySerializer.new(
users,
each_serializer: UserSerializer
).to_json
end


user_serialiser.rb

class UserSerializer < ActiveModel::Serializer

attributes :id, :email, :name

def name
object.display_name
end
end


jsonの配列を作る書き方は色々ありますが、jbuilderの場合と同様にuserの情報を他のところからも呼び出せるようにUserSerializerを作っておきました。

同じデータを検索したところ、serializerを使った場合は100件で 40ms〜50ms くらいでした。1/7くらいでしょうか

200件300件と増えれば増えるほど差が開きます。


テストが書きやすい

シリアライズするだけのクラスが出来るのでそのクラスをテストするのもやりやすいです。

ARのModelの中にas_jsonやto_builderを書いていくと、いくつか出力したいjsonのパターンを準備したくなると途端に条件分岐や他のメソッドの呼び出しが増えて煩雑になることがあります。

テストも書きにくくなります。

ActiveModel::Serializerを継承してますが比較的シンプルなクラスなのでテストも普通のクラスについて書くように書けるので楽です。


難点

自分はあまり困ってないですが、場合によっては難点かもなと思うところもあります。


ActiveModelに依存する

名前の通り、ActiveModelに依存しています。

ActiveModel::ModelとActiveModel::Serializationを使います。

僕はActiveModelはActiveModel::Validationsとか便利だし、Rails使おうとPadrino使おうと永続化にはActiveRecord使いたいのでActiveModelに依存しちゃっても全然構わないのですが、ActiveModelを利用したい訳じゃない時には嫌なことはあるかも。


次のバージョンで変更が大きそう

現在0.8.xが安定版なんですが、今度出る0.10.xは色々変わりそうです。

まあこれはどのgemでもあるかなと思いますし、じっくりウォッチするしかないですね。

ドキュメントも変更中のせいか量がすごく減っているので0.8のREADMEを見るようにしています。


困った時


ActiveRecord::Baseではないクラスで使いたい

ActiveModel::ModelとActiveModel::Serializationをincludeしたらserializerに渡して使えます。

こういう感じ

class Post

include ActiveModel::Serialization
include ActiveModel::Model
end


なにか値をserializerに渡したい

こまったらoptionsを渡せば何とかなります。

ActiveModel::ArraySerializer.new(

users,
each_serializer: UserSerializer,
message: message # serializer内でoptions[:message]で呼び出せる
)

ただ、これは今度出る0.10では無くなるかもしれません。

代わりのものが候補に挙がってるようですがこれというのが決まってないようです。


関連するモデルを出力したい

has_manyやhas_oneも使えます(あくまで多重度が1か多かを示すだけなのでbelongs_toはありません)

has_many: :postsみたいに書けます。

どのserializerを使うかも指定できるので関連したモデルの必要なattributesだけ出すのも簡単です。

has_many: :posts, serializer: ShortPostSerializer


ジェネレータを使いたい

ジェネレーターもあります。

rails g serializer post


というわけで

ActiveModel::Serializerでjson周りの困ってたことがだいたい解決しました。

スピードが速くてモデルと切り離せるのがとても良いです。