Rails APIでSerializerを使ってjson形式の出力データを整形する方法
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
次に、ルーティングも設定しておきましょう。
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
するところで直接渡す属性を指定しても良いかもしれません。
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" }
もしくは、すべての要素を出力する場合は、そもそも指定なんかする必要はありません。
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_at
やcreated_at
のような、自動で生成される属性だって持っているし、レスポンスに渡したい値が1, 2個だけで済まないことの方が多いでしょう。
ここで、私がSerializerを導入する前に行っていた方法も紹介しておきます。
4.2. モデルにデータ整形メソッドを書く
基本的に、レスポンスにデータを渡す際、Userモデルのインスタンスの属性を出力したいとか、Articleモデルのインスタンスの属性を出力したいとか、モデルのインスタンスの一部を出力したいことが多いです。
そのため、モデルにデータ整形メソッドを書いてしまえば、コントローラが分厚くなることを防げます。
例えば、以下のようなコントローラなら、
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
モデルにデータ整形メソッドを定義することで次のように表せます。
class ArticlesController < ApplicationController
@article = Article.find(params[:id])
render status: :ok, json: @article.to_json
end
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
を行いましょう。
gem "active_model_serializers"
bundle install
これでSerializerを使用する準備は整いました。それでは、Serializerを作成していきましょう。
rails g serializer Article
gemを入れるだけで、rails generate
までできるようになっているので、直感的に使いやすいです。
コマンドを実行すると、appの下にserializersフォルダの下にファイルが生成されているのが確認できます。
5.1. 基本的な使い方
ここで一度、先ほどのコントローラを確認しておきましょう。
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を使って定義すると、以下のようになります。
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
render status: :ok, json: @article, serializer: ArticleSerializer
end
end
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
オプションをつけてあげます。
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
に入っているとして、それを渡してやるには、以下のようにすれば良いです。
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
@favoritesCount = 10
render status: :ok, json: @article,
serializer: ArticleSerializer, favoritesCount: @favoritesCount
end
end
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のインスタンスを生成して表示すると、このように書けます。
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
に変わっているくらいですね。
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オプションを使うと楽です。
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をインスタンスとして生成するやり方でやってみましょう。
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