🐥

Rails APIでSerializerを使ってjson形式の出力データを整形する方法

2024/01/04に公開

1. はじめに

RailsをAPIモードで作成していると、通常のRailsアプリと異なり、viewがありません。そして、コントローラではHTTPステータスとjsonデータを返してやる必要があります。

しかし、Railsは基本的にjson形式で変数を管理していないので、レスポンスを返す際にデータをjson形式で整形する必要性がでてきます。

初めはモデルにto_jsonメソッドを定義して、レスポンスを返す際にそれを使用していたのですが、Articleモデルのインスタンスの配列をjson形式で整形する際に、困ってしまいました。

という訳で、出力時のデータ整形のためのメソッドをどこに置いたら良いか悩んだので、今回使用したSerializerについて記事を書いていきます。

2. Serializerとは

まず、Serializerとは何か。

データを保存したり、ネットワークで送受信できるようにしたりするものです。

Rails active_model_serializerとは #Rails - Qiita より引用

とのことですが、Railsで使われる際は、意味合いが少し変化してるのかなと思いました。

色々とRailsでSerializerについて書いている記事を読んだ感想としては、出力時のデータ整形メソッドをまとめておく機能としてとらえられていると感じました。

Serializerについては、いくつかのGemが候補に挙がると思いますが、今回は一番よく見かけるactive_model_serializersを使っていきます。

そのために、まずはRailsアプリの準備からやっていきます。Serializerの使い方から読みたい方は5. モデルのインスタンスにSerializerを適用まで飛んでください。

3. Railsアプリの準備

新しいRailsアプリを作成して、試してみます。

まず、Articleの属性を取得したいので、モデルが必要です。

rails g modle Article title:string description:string body:string

次に、ルーティングも設定しておきましょう。

routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :articles, only %i[index show]
  end
end

rails routesで確認すると、ルーティングは以下のようになっています。

Helper HTTP Verb Path Controller#Action
articles_path GET /api/articles(.:format) articles#index
article_path GET /api/articles/:id(.:format) articles#show

表示するためには、実際にArticleモデルのインスタンスが必要なので、コンソールで作ってしまいましょう。

rails c
article = Article.new(title: "title dayo", description: "description dayo", body: "body dayo")
article.save

最後にコントローラを作れば準備は完了です。

rails g controller Articles

4. 問題意識の共有

4.1. コントローラにそのまま書いてみる

APIでは、viewを作成せず、コントローラからjson形式で何のデータをレスポンスとして渡すかを設定します。

例えば、Articleモデルのインスタンス@articleがあったときに、この中でid属性は渡す必要ないなとか、title属性は渡したいなとか、そういったことを書いていく必要があります。

渡す要素が少なかったら、renderするところで直接渡す属性を指定しても良いかもしれません。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    render status: :ok, json: { title: @article.title, body: @article.body }
  end
end
{ "title":"title dayo",
  "body":"body dayo" }

もしくは、すべての要素を出力する場合は、そもそも指定なんかする必要はありません。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    render status: :ok, json: @article
  end
end
{ "id":1,
  "title":"title dayo",
  "description":"description dayo",
  "body":"body dayo",
  "created_at":"2024-01-04T02:00:10.239Z",
  "updated_at":"2024-01-04T02:00:10.239Z" }

ですが、実際はそうではないことの方が多いと思います。

updated_atcreated_atのような、自動で生成される属性だって持っているし、レスポンスに渡したい値が1, 2個だけで済まないことの方が多いでしょう。

ここで、私がSerializerを導入する前に行っていた方法も紹介しておきます。

4.2. モデルにデータ整形メソッドを書く

基本的に、レスポンスにデータを渡す際、Userモデルのインスタンスの属性を出力したいとか、Articleモデルのインスタンスの属性を出力したいとか、モデルのインスタンスの一部を出力したいことが多いです。

そのため、モデルにデータ整形メソッドを書いてしまえば、コントローラが分厚くなることを防げます。

例えば、以下のようなコントローラなら、

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    render status: :ok, json: { title: @article.title, 
				body: @article.body, 
				updatedAt: @article.updated_at }
  end
end

モデルにデータ整形メソッドを定義することで次のように表せます。

article_controller.rb
class ArticlesController < ApplicationController
  @article = Article.find(params[:id])
  render status: :ok, json: @article.to_json
end
models/article.rb
class Article < ApplicationRecord
  def to_json
    { title: title, 
      body: body, 
      updatedAt: updated_at }
  end
end

出力は以下です。

{ "title":"title dayo",
  "body":"body dayo",
  "updatedAt":"2024-01-04T02:00:10.239Z" }

コードの規模が小さければ、もしかしたらこれで十分かもしれません。

しかし、これはモデルのインスタンスを単体で出力する場合にしか使えない手段です。実際、私はArticleモデルのインスタンスの配列を出力する必要が生じたタイミングで、困ってしまいました。

これは配列であってモデルではないので、上で定義したto_jsonメソッドは使えません。そこで登場するのがSerializerです。

5. モデルのインスタンスにSerializerを適用

まずはGemfileにactive_model_serializersを加えて、bundle installを行いましょう。

Gemfile
gem "active_model_serializers"
bundle install

これでSerializerを使用する準備は整いました。それでは、Serializerを作成していきましょう。

rails g serializer Article

gemを入れるだけで、rails generateまでできるようになっているので、直感的に使いやすいです。

コマンドを実行すると、appの下にserializersフォルダの下にファイルが生成されているのが確認できます。

5.1. 基本的な使い方

ここで一度、先ほどのコントローラを確認しておきましょう。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    render status: :ok, json: { title: @article.title, 								body: @article.body, 
				updatedAt: @article.updated_at }
  end
end

これをSerialierを使って定義すると、以下のようになります。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    render status: :ok, json: @article, serializer: ArticleSerializer
  end
end
article_serializer.rb
class ArticleSerializer < ActiveModel::Serializer
  attributes %i[title body updatedAt]

  def updatedAt
    object.updated_at
  end
end

attributesを使って、表示する変数を設定しています。設定すると、何も定義していない場合はモデルのインスタンスが持つ属性を探して、その値を出力するように設定してくれます。

updated_atは表示するテキストを少し変更したいので、updatedAtメソッドを定義して中身の情報が何になるかを明示してあげています。

その際にでてくるobjectは、コントローラ内でのselfのような役割を果たすもので、ここではrender時に設定した@articleを表します。Serializerの中では、インスタンスはobjectで表します。

そして、出力を確認すると以下のようになっています。

{ "title": "title dayo", 
  "body": "body dayo", 
  "updatedAt": "2024-01-04T02:00:10.239Z" }

5.2. ルートキーの変更

出力がこれだけだと少し物足りません。実際には、ある属性たちをひとかたまりとみなして、名前を付けてあげたいということがよくあるはずです。

例えば、先ほどの出力データにarticleという名前をつけてくくってあげるにはコントローラのrender時にrootオプションとadapterオプションをつけてあげます。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    render status: :ok, json: @article, 
			serializer: ArticleSerializer, root: "article", adapter: :json
  end
end

すると、出力は次のように変化します。

{ "article":
  { "title":"title dayo",
    "body":"body dayo",
    "updatedAt":"2024-01-04T02:00:10.239Z" } }

5.3. 引数をとる

さらに言うなら、モデルに含まれていない属性を、レスポンスに含めたいこともあるでしょう。

そういった場合は、Serializerに引数を渡してやれば良いです。

記事がお気に入りされている数が@favoritesCountに入っているとして、それを渡してやるには、以下のようにすれば良いです。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @favoritesCount = 10
    render status: :ok, json: @article, 
			serializer: ArticleSerializer, favoritesCount: @favoritesCount
  end
end
article_serializer.rb
class ArticleSerializer < ActiveModel::Serializer
  attributes %i[title body favoritesCount]

  def initialize(object, options = {})
    super(object)
    @favoritesCount = options[:favoritesCount]
  end

  def favoritesCount
    @favoritesCount
  end
end

ここで何が起きたのか少し説明しておくと、

まず、seializer: ArticleSerializerの指定からこのクラスが呼び出されているわけですが、serializerを読み込む際に、適当なハッシュをオプションとして渡すことができます。

渡した上で、このクラスを読み込む際に最初に実行されるinitializeメソッドを定義してやることで、optionsからこのクラス内で使える変数として@favoriteCountを定義してやります。インスタンス変数にすることで、ArticleSerializerクラスの他のメソッドでもこの@favoriteCountが参照できるようになります。

また、インスタンス自身を表すobjectも実はrenderの引数からとってきていて、それをobjectとして定義するのを親クラスで実行してくれています。

しかし、initializeメソッドをオーバーライドしてしまったことで、明記しないと親クラスのinitializeメソッド内のコードを実行しなくなっているので、super(object)を明記することで、こいつがインスタンスですよーという処理を行っています(多分)。

後は、updatedAtに自由な名前を付けたときと同じ要領で、名前をつけてやれば良いわけです。

すると、出力は以下のようになります。

{ "title":"title dayo",
  "body":"body dayo",
  "favoritesCount":10 }

もちろん、ルートキーの変更と引数をとるのを同時に行うこともできます。

5.4. 違う書き方

今まではrenderに渡す引数としてSerializerを使ってきたわけですが、Serializerのインスタンスを生成して、コントローラ内で細かくデータ形式をいじることもできます。

5.2の出力をSerializerのインスタンスを生成して表示すると、このように書けます。

article_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    @favoritesCount = 10
    render status: :ok, json: { 
      article: ActiveModelSerializers::SerializableResource.new(@article, serializer: ArticleSerializer)
    }
  end
end

そして、出力は以下のようになります。

{ "article":
  { "title":"title dayo",
    "body":"body dayo",
    "updatedAt":"2024-01-04T02:00:10.239Z" } }

6. モデルのインスタンスの配列にSerializerを適用

次は、モデルのインスタンスの配列に対してSerializerを適用する場合です。

まず、わかりやすいようにもう一つArticleモデルのインスタンスを作成しておきましょう

rails c
article = Article.new(title: "title2 desu", description: "description2 desu", body: "body2 desu")
article.save

6.1. 基本的な使い方

基本的な使い方は、単体のときとほとんど変わりありません。

オプションのキーがserializerからeach_serializerに変わっているくらいですね。

article_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
    render status: :ok, json: @articles, 
			each_serializer: ArticleSerializer
  end
end

このとき、出力は以下のようになります。

[
  { "title":"title dayo",
    "body":"body dayo",
    "updatedAt":"2024-01-04T02:00:10.239Z" },
  { "title":"title2 desu",
    "body":"body2 desu",
    "updatedAt":"2024-01-04T03:15:04.042Z" }
]

6.2. モデルの配列にmetaオプションを付与

ルートキーの指定や、変数の受け渡しはモデル単体の時と同様に行うことができますが、配列の場合だと、配列の最後とかに特別に値をいれたいことがあると思います。Articlesの数とか。

そういった場合には、metaオプションを使うと楽です。

article_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
    render status: :ok, json: @articles, 
			each_serializer: ArticleSerializer, 
			meta: { articleCount: Article.count }, adapter: :json
  end
end

すると、出力は次のようになります。

{
  "articles":
  [
    { "title":"title dayo",
      "body":"body dayo",
      "updatedAt":"2024-01-04T02:00:10.239Z" },
      { "title":"title2 desu",
	"body":"body2 desu",
	"updatedAt":"2024-01-04T03:15:04.042Z" }
  ],
  "meta":{ "articleCount":2 }
}

6.3. metaという名前は使いたくない

これはとても楽ですが、metaという名前がついてほしくない場合もあると思います。

この書き方でこれ以上細かくカスタマイズするのはちょっと難しいので、Serializerをインスタンスとして生成するやり方でやってみましょう。

article_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
    render status: :ok, json: {
      articles: ActiveModel::Serializer::CollectionSerializer.new(@articles, serializer: ArticleSerializer),
      articlesCount: @articles.size
    }
  end
end

このとき、出力は次のようになります。

{
  "articles":
  [
    { "title":"title dayo",
      "body":"body dayo",
      "updatedAt":"2024-01-04T02:00:10.239Z" },
    { "title":"title2 desu",
      "body":"body2 desu",
      "updatedAt":"2024-01-04T03:15:04.042Z" }
  ],
  "articlesCount":2
}

7. まとめ

  • Railsアプリでデータをjson形式で出力する際、active_model_serializerを使うと楽
  • ざっくりで良いときはrenderのオプションとしてseriarlizerを渡す
  • 細かく決めたいときは、serializerのインスタンスを生成し、コントローラ内で直接レスポンスの細かい形式を指定する。

8. 最後に

モデル内にデータ整形メソッドを書いていたのもあり、インスタンスの配列だけ別扱いするのもな...ともやもやしているところで、Serializerについて知りました。

使えるからいいや精神で理解を省いているところもありますが、最低限必要なところは抑えられたかなと思います。

この記事が、少しでもRailsでAPIを作成している方の助けとなれば幸いです。

参考記事

Discussion