ActiveModel::Serializersでリソースに応じて動的にSerializerを切り替える

この記事は ZOZOテクノロジーズその2 Advent Calendar 2018 - Qiita 13日目の記事です。

今回はActiveModel::Serializers(AMS)の便利メソッドを紹介します。 github.com

AMSを使ってjsonシリアライズ実装をしていたのですが、以下のような非ActiveRecordオブジェクトのシリアライズ
動的にSerializerを切り替えたい場面で困った事がありました。

困った事例

ワールドトリガーを例にサンプルコードを書きます。
人物とトリガーを表すクラスが存在し、人物オブジェクトをAMSをつかってシリアライズする実装を考えてみます。

クラス定義

# 人物は1つだけトリガーを持つことができるとする
class Person < ActiveModelSerializers::Model
  attributes :name, :trigger
end

# ノーマルトリガーはオプション装備をひとつ文字列で指定可能
class NormalTrigger < ActiveModelSerializers::Model
  attributes :name, :option
end

# 黒トリガーは製作者の名前を持つ
class BlackTrigger < ActiveModelSerializers::Model
  attributes :maker
end

実際にオブジェクトを作成

normal_trigger = NormalTrigger.new(name: 'レイガスト', option: 'スラスター')
osamu = Person.new(name: '三雲 修', trigger: normal_trigger)

black_trigger = BlackTrigger.new(maker: '空閑 有吾')
yuma = Person.new(name: '空閑 遊真', trigger: black_trigger)

各トリガーに対応するシリアライザを作成

class NormalTriggerSerializer < ActiveModel::Serializer
  attributes :name, :option
end

class BlackTriggerSerializer < ActiveModel::Serializer
  attributes :maker
end

最後にPersonに対応するSerializerを作成
triggerオブジェクトに2種類のクラスのオブジェクトが入る可能性があり、動的にSerializerを切り替えてやる必要があって困りました。

class PersonSerializer < ActiveModel::Serializer
  attribute :name, :trigger

  def trigger
    # ノーマルトリガー、黒トリガー2種類のクラスのオブジェクトを扱うため、動的にSerializerを切り替えたい
    NormalTriggerSerializer.new(object.trigger) # ノーマルトリガーの場合
    BlackTriggerSerializer.new(object.trigger)  # 黒トリガーの場合
  end
end

解決法

上記のようにシリアライザの中で複数のSerializerを使い分ける場合、どのSerializerを呼べばいいかわからない問題に直面しますが ActiveModelSerializers::SerializableResource#serializer_instanceを使用することで1行で回避可能です。

class PersonSerializer < ActiveModel::Serializer
  attributes :name, :trigger

  def trigger
    ActiveModelSerializers::SerializableResource.new(object.trigger).serializer_instance
  end
end

この他ActiveModelSerializers::SerializableResource#serializerメソッドは与えられたリソースからSerializerクラスを返します。
これらのメソッドを利用することで、リソースから動的に対応するSerializerクラスやそのインスタンスを取得可能です。

参考