RailsでAPIを作成しようとしたら、
gem 'active_model_serializers'
をbundle installすることで使用できるActiveModel::Serializerを使うことは多いかと思います。
erbやslim等のテンプレートエンジンを使ったモノリスのRailsしか知らない人からすると、初めてのことで色々と戸惑うことも多いでしょう。
私もそのひとりでした。
基本から、私の苦労したカスタマイズまで、備忘録として残しておきます。
ActiveModel::Serializer(active_model_serializers)とは
jsonを出力してくれるgemです。
詳しいことは外部サイトに譲ります。こちらがわかりやすかったです。
【RailsAPI】active_model_serializersの基礎からまとめてみた(実践編もあるよ)
基本のキ
app/models/parent.rb:
class Parent < ApplicationRecord
has_many :users, dependent: :destroy
end
app/models/user.rb:
class User < ApplicationRecord
belongs_to :parent
has_many :children, dependent: :destroy
end
app/models/child.rb:
class User < ApplicationRecord
belongs_to :user
end
今回は上記モデルを用意して、下記シリアライザーファイルを作成します。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name]
end
これは、このような記述でも同じです。
class UserSerializer < ActiveModel::Serializer
attribute :id
attribute :name
end
複数ある場合は上、カラムが単体の場合は下の記述が良さそうですね。
キ1:単体を出力
コントローラで下記のように記載してrenderすればOKです。
app/controllers/users_controller.rb:
class UserController < ApplicationController
def show
@user = User.find(params[:id])
render json: @user, serializer: UserSerializer
end
end
Response bodyの出力結果:
{
id: 1,
name: "太郎"
}
以降、行数がかさむので下記のようにも記載します。
{ id:1, name:"太郎" }
キ2:配列を出力
each_serializerを使用してrenderすればOKです。
app/controllers/users_controller.rb:
class UserController < ApplicationController
def index
@users = User.all
render json: @users, each_serializer: UserSerializer
end
end
Response bodyの出力結果:
[
{ id:1, name:"太郎" },
{ id:2, name:"花子" }
]
キ3:ルートキーの有無の切り替え
上記の例では、ルートキー(”user:”みたいなやつ)が省略された形で出力されました。これは、adapterオプションのデフォルトが:attributesだからです。これを明示的に記述すると、
app/controllers/users_controller.rb:
(以後、関係する箇所だけ抜粋)
render json: @user, serializer: UserSerializer, adapter: attributes
Response bodyの出力結果:
{ id:1, name:"太郎" }
先ほどと同じ結果になりました。
ルートキーを表示したい場合は、下記のようにadapterオプションに:jsonを指定します。
app/controllers/users_controller.rb:
render json: @user, serializer: UserSerializer, adapter: :json
Response bodyの出力結果:
{
user: { id:1, name:"太郎" }
}
ここまでが基本中の基本です。
基本のホ(規約の範囲内なら簡単)
CoC(設定より規約)←つまり規約の範囲内なら、少しくらい複雑なことでも簡単に実装できます。
ホ1:ルートキーの変更
ルートキーをモデル名とは別の何かに変更するにはtypeオプションを使えばOK
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
type 'account_info'
attributes %i[id name]
end
Response bodyの出力結果:
{
account_info: { id:1, name:"太郎" }
}
ちょくちょく使います。
ちなみに、コントローラーでroot: XXXを指定することでも、ルートキーを変更できます。
app/controllers/users_controller.rb:
render json: @user, serializer: UserSerializer, adapter: :json, root: 'account_info'
呼び出すAPIによって異なるルートキーが要求される場合は、こちらの方が便利ですね。
どちらも設定した場合はrootの方が優先されるので、デフォルトはtypeで設定しておいて、さらにAPI毎にカスタマイズしたい場合はrootで設定する、ということも可能です。
また、詳細は別記事にしますが、ただの値や配列などnon-ActiveRecord オブジェクトのシリアライズをする場合も、この「ルートキーの変更」と同じ対応が必要なので、ぜひ覚えておいてください。
値や配列などnon-ActiveRecord オブジェクトのシリアライズをする(Rails7)
ホ2:モデルのカラムにないキーを出力
これもよく使いますね。例えば、持っている子テーブルの数を’children_count’で出力したい、とか。こういう場合は、キー名をメソッド名にしてメソッドを用意してあげます。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name children_count]
def children_count
object.children.all.size
end
end
Response bodyの出力結果:
{ id:1, name:"太郎", children_count: 3 }
“object”で、コントローラで指定したobject(今回の例だと@user)を使うことができます。
ちなみに、上記は他の表現方法も可能です。下記に例を挙げておきます。
(1) attributeブロックのみを使用
class UserSerializer < ActiveModel::Serializer
attributes %i[id name]
attribute :children_count do
object.children.all.size
end
end
“children_count”を2箇所に書いていたのが、1箇所で済むのでより変更に強いコードになります。ただ、procのため returnは使えないようですし、メソッドとして他で引用することもできない点はデメリットですね。
(2) attributeブロックをワンライナーで使用
class UserSerializer < ActiveModel::Serializer
attributes %i[id name]
attribute(:children_count) { object.children.all.size }
end
今回の例のように1行で短い場合なら、この方がシンプルで良いですね。
(3) モデルファイルに記述
モデルファイルに記述して疑似カラムにしてあげても同様に機能します。json出力でしか使わない場合はserializerで、他でも使う場合はモデルファイルに記述してあげると良さそうですね。
class User < ApplicationRecord
(...途中省略...)
def children_count
self.children.all.size
end
end
ホ3:コントローラーから引数を渡して出力
引数を設定したら、@instance_optionsで受け取れます。試しに、current_userにログインユーザーのユーザーオブジェクトが格納されているとして、login_userというキーに渡してみましょう。
app/controllers/users_controller.rb:
render json: @user, serializer: UserSerializer, login_user: current_user
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name login_user_name]
def login_user_name
login_user.name
end
def login_user
@instance_options[:login_user]
end
end
Response bodyの出力結果:
{ id:1, name:"太郎", login_user_name: "花子" }
@userに関係のない、ログインユーザーの情報を出力することができました。
login_user_nameメソッドの中で@instance_options[:login_user]をそのまま使うこともできるのですが、可読性・拡張性を考慮して上記のように定義してしまう方が私は好きです。「@instance_options」なんてここでしか見たことないので。
scopeを使う方法もあるが、渡された引数が何なのか、◯◯_serializer.rbだけ見た場合にわかりにくいので、非推奨とのことです。
ホ4:メタ情報を追加
ルートキーと同じ階層にmetaオプションを使用すればOKです。そのときはルートキーはありにする方が良いですね。
app/controllers/users_controller.rb:
class UserController < ApplicationController
def index
@users = User.all
render json: @users,
each_serializer: UserSerializer,
meta: meta_info,
adapter: :json
end
private
def meta_info
{
hoge: "fuga",
foo: "bar"
}
end
end
Response bodyの出力結果:
{
users: [
{ id:1, name:"太郎" },
{ id:2, name:"花子" }
],
meta: {
hoge: "fuga",
foo: "bar"
}
}
ただし、このキー名”meta”は固定のようです。(この”meta”をいじりたいと思ったら、本記事後半で述べるカスタマイズの1や5を駆使する必要があります。)
ページネーションの情報や、オブジェクトとは関係ない情報を出力するのに良さそうですね。
ホ5:親を表示
モデルで定義したとおりなら簡単に出力できます。
別途、親モデルもserializerを定義しておきます。
app/serializers/parent_serializer.rb:
class ParentSerializer < ActiveModel::Serializer
attributes %i[id parent_name]
end
そして、モデル定義と同様、belongs_toを使います。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name]
belongs_to :parent, serializer: ParentSerializer
end
これでOKです。下記のように出力されます。
Response bodyの出力結果:
{
id: 1,
name: "太郎",
parent: { id: 1, parent_name: "健太郎" }
}
自身のカラムと、親のキー(ここではparent)が並列で出力されます。これをuserとparentを並列で出力したいと思ったら・・・一気に面倒になります。その場合、後述するカスタマイズが必要です。
ホ6:子を表示
こちらも親と同様です。別途、子モデルをserializerを定義しておきます。
app/serializers/child_serializer.rb:
class ChildSerializer < ActiveModel::Serializer
attributes %i[id child_name]
end
そして、モデル定義と同様、has_manyを使います。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name]
has_many :children, serializer: ChildSerializer
end
これでOKです。下記のように出力されます。
Response bodyの出力結果:
{
id: 1,
name: "太郎",
children: [
{ id: 1, child_name: "いちろう" },
{ id: 2, child_name: "じろう" },
{ id: 3, child_name: "さぶろう" }
]
}
1対多の関係だと、子レコードは配列で出力されることに注意してください。1対1(has_one)なら、単体で出力されると思います。
ホ7:親や子を出力したいが、順番を制御したい
親子を出す順番は簡単に制御できます。attributesの中で指定してあげればOKです。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id parent name]
belongs_to :parent, serializer: ParentSerializer
end
これでOKです。下記のように出力されます。
Response bodyの出力結果:
{
id: 1,
parent: { id: 1, parent_name: "健太郎" },
name: "太郎"
}
カスタマイズが面倒(ここが本題)
ここからは、CoC(設定より規約)の考え方を少しずつ逸脱して参ります。やはりRailsは自由にカスタマイズしようとすると、記述量がとたんに多くなりますね。
カ1:キーを変えて親や子を出力したい場合
ルートキーなら、typeオプションでいけるんですが、リレーションのレコードを出力する際は、typeオプションが効きませんでした・・・
そこで、ActiveModelSerializers::SerializableResourceをnewしてあげる技を身につけましょう。
子レコードのうち、first_childというキーの名前で、最初のレコードだけ出力してみます。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name first_child]
def first_child
ActiveModelSerializers::SerializableResource.new(
object.children.first, serializer: ChildSerializer
).serializable_hash
end
end
Response bodyの出力結果:
{
id: 1,
name: "太郎",
first_child: { id: 1, child_name: "いちろう" }
}
whereで絞って子を出力したい場合も、この方法を使う必要があります。逆に、この方法を使えば、全く関係ないモデルも出力することができます。コントローラー内でも使えます。
カ2:条件によって出し分けをしたい(1つ)
例えば、レコードがないときは出力しないようにするには、下記のようにできます。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name]
attribute :first_child, if: :children_present?
def first_child
ActiveModelSerializers::SerializableResource.new(
object.children.first, serializer: ChildSerializer
).serializable_hash
end
def children_present?
object.children.first.present?
end
end
children_present?がfalseの場合のResponse bodyの出力結果:
{
id: 1,
name: "太郎"
}
trueならカ1と同じように出力されます。
カ3:条件によって出し分けをしたい(丸々)
丸々出し分けるにはwith_optionsを使います。今度は、子レコードがあるときは本体を出力しないで親子のみ出力するようにしてみます。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
with_options unless: :children_present? do
attribute :id
attribute :name
end
with_options if: :children_present? do
belongs_to :parent, serializer: ParentSerializer
has_many :children, serializer: ChildSerializer
end
def children_present?
object.children.first.present?
end
end
children_present?がfalseの場合のResponse bodyの出力結果:
{
id: 1,
name: "太郎"
}
children_present?がtrueの場合のResponse bodyの出力結果:
{
parent: { id: 1, parent_name: "健太郎" },
children: [
{ id: 1, child_name: "いちろう" },
{ id: 2, child_name: "じろう" },
{ id: 3, child_name: "さぶろう" }
]
}
だいぶコードが冗長になってきましたね・・・
カ4:レコードがないときもキーは出力したい
キーだけは出力する場合、メソッド内でnil回避します。
app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes %i[id name first_child]
def first_child
return nil if object.children.first.blank?
ActiveModelSerializers::SerializableResource.new(
object.children.first, serializer: ChildSerializer
).serializable_hash
end
end
レコードがない場合のResponse bodyの出力結果:
{
id: 1,
name: "太郎",
first_child: nil
}
配列が返る場合は”[]”を出力してあげると良さそうですね。
カ5:複数のシリアライザーを出力
コントローラで2つのjsonをmergeで結合させてあげる必要があります。
app/controllers/users_controller.rb:
class UserController < ApplicationController
def show
@user = User.find(params[:id])
render json: user_json.merge(children_json)
end
private
def user_json
ActiveModelSerializers::SerializableResource.new(
@user, serializer: UserSerializer, adapter: :json
).serializable_hash
end
def children_json
ActiveModelSerializers::SerializableResource.new(
@user.children.all, each_serializer: ChildSerializer, adapter: :json
).serializable_hash
end
end
Response bodyの出力結果:
{
user: { id:1, name:"太郎" },
children: [
{ id: 1, child_name: "いちろう" },
{ id: 2, child_name: "じろう" },
{ id: 3, child_name: "さぶろう" }
]
}
親子関係のはずのuserとchildrenが並列で表示されているのがわかるでしょうか。結構単純な構成なのに、これをserializerで出そうとすると、それぞれ出力してmergeしてあげる必要があるという・・・もっと簡単な方法をご存知の方いらっしゃいましたら、コメントで教えていただけると幸いです。
参考資料
【Rails】ActiveModelSerializersの導入
Active Model Serializer をざっくり使ってみた
【Rails/ActiveModelSerializers】each_serializerで呼び出し側から、Serializerクラスに値を渡したいとき
active model serializers のレシピ集 & 個人的ベストプラクティス集
gem Active Model Serializers のドキュメントを翻訳しました
最後まで読んでくださってありがとうございました!
コメント