開発ノート@HarikoApps

HarikoApps: https://hariko.sprkls.me

2024-06-21

ActiveModel::Serializers::JSON を使ったシンプルなシリアライザ #11

RailsでJSON APIを実装しているプロジェクトでシリアライザがなかったものにシリアライザを導入した際に、Railsに含まれるActiveModel::Serializers::JSONを用いたクラスを使ようにしてみました(確認環境:Rails 7.1.3.4)。

下記のようにベースクラスを定義し、適宜継承して使うような実装にしました。ドキュメントにattributesメソッドを定義するように書かれているのでそれに従い、簡潔に書けるようにexposed_attribute_namesというメソッドを子クラスでオーバーライドするようにしています。

class BaseEntity
  include ActiveModel::Serializers::JSON

  def exposed_attribute_names
    []
  end

  def attributes
    # Attribute名の配列を、Attribute名をキーにしたnil値をもつハッシュに変換
    # 例) [:id, :name] => { "id" => nil, "name" => nil }
    exposed_attribute_names.to_h { |key| [key.to_s, nil] }
  end
end

class ArticleEntity < BaseEntity
  def initialize(article)
    @article = article
  end
  attr_reader :article
  delegate :title, :body, to: :article

  def exposed_attribute_names
    %i[title body]
  end
end
コントローラーでの使い方は次のようになります。

class ArticlesController < ApplicationController
  def show
    article = Article.find(params[:id])
    render json: ArticleEntity.new(article)
  end
end
とてもシンプルな実装なので個々のエンティティは少し冗長な書き方になってしまうことは否めませんが、その分柔軟でさまざまな状況に対応できます。

元々、この手法を知ったのはSTORES.jpさんのブログ記事でした(多謝!)

追記)

このシリアライザを使っているプロジェクトのメンバーから、JSONをルートプロパティでラップできるようにしたいという要望があり対応しました。

{ title: "Hello!", body: "..." }
のようなJSONをレンダリングするシリアライザオブジェクトが、

{ article: { title: "Hello!", body: "..." } }
のようなJSONを作るようにしたいという要件です。このような場合、ActiveModel::Serializers::JSONにはすでに include_root_in_json というClass Attributeが定義されており、シリアライザのモデル定義の中で self.include_root_in_json = :article のようにしたりすると上記のような要件にも対応できます。今回の場合、これを設定するためのAPI向けシリアライザのドメインを表現するようなメソッドを用意しました。

  def self.wrap_exposed_attributes_with(wrap_name)
    self.include_root_in_json = wrap_name
  end
次のようにエンティティクラスから使用します。

class ArticleEntity < BaseEntity
  wrap_exposed_attributes_with :article
  ...
end

# ArticleEntity.new(article).as_json #=> { article: { title: "...", body: "..." } }
Updated at