ActiveModel::Serializerを使いこなす(Rails7)

Rails・Webシステム開発
Anja🤗#helpinghands #solidarity#stays healthy🙏によるPixabayからの画像
スポンサーリンク

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 のドキュメントを翻訳しました

はるすと
はるすと

最後まで読んでくださってありがとうございました!

この記事を書いた人
こもれびエンジニア

自然と自由を愛するエンジニア。2021年1月に、大手製造業設計からプログラマ(Rails, AWS)へ転職。動物や自然との触れ合いや、汗を流すのが好き。

/HSP(繊細さん)/18デリケートな象/ストレングスファインダー(1分析思考/2親密性/3学習欲/4調和性/5収集心)、テニス、合気道、登山、あいだみつを、ジブリ、ワンピース、ドラゴンボール、AWS、Ruby on Rails、アイミング

twitterをフォローして、記事にならないちょっとした豆知識もチェック!
Rails・Webシステム開発
スポンサーリンク
SNSでシェア/コメントして、自分のアウトプット/発信力を高めるのにお使いください。 ↓ 各ページへジャンプ ↓
twitterをフォローして、記事にならないちょっとした豆知識もチェック!
スポンサーリンク
「そんなか」サイト

コメント

タイトルとURLをコピーしました