1. 結論(この記事で得られること)
この記事を読むと、以下が明確になります。
- ActiveModelSerializerとJbuilderの使い分け基準が実務レベルで判断できる
- それぞれの落とし穴とパフォーマンス特性を理解し、レビューで指摘できる
- AIを使って既存コードの変換やテスト生成を10倍速で進められる
- 実際のプロダクトで起きた失敗事例から学べる
私自身、最初のプロジェクトでは「なんとなくJbuilderを使っていた」んですが、APIが複雑化してきたタイミングで地獄を見ました。この記事では、そういった痛みを避けるための実践知識を凝縮してお届けします。
2. 前提(環境・読者層)
想定環境
- Rails 7.x系(6.x系でも大半は適用可能)
- active_model_serializers gem 0.10.x
- JSON API が中心のプロダクト
想定読者
- Railsで API 開発を担当している
- Serializer層の設計に悩んでいる
- 既存のJbuilderをリファクタリングしたい、または逆にAMSから移行を検討している
- レビュアーとして設計判断の根拠を示したい
3. Before:よくあるつまずきポイント
3-1. 「どっち使えばいいの?」問題
まず多くの人がハマるのがここ。Railsには標準でJbuilderが入ってるので、なんとなくそのまま使い始めがちです。
# app/views/api/users/show.json.jbuilder
json.id @user.id
json.name @user.name
json.email @user.email
json.posts @user.posts do |post|
json.id post.id
json.title post.title
end
最初はシンプルでいいんですが、以下のようなケースで破綻します。
- 複数のエンドポイントで同じuser表現が必要になる → コピペ地獄
- API バージョニングが必要になる → view ファイルが爆増
- N+1が発生しても気づきにくい → includes忘れでパフォーマンス劣化
3-2. ActiveModelSerializerを導入したら逆に困った
一方で、「オブジェクト指向っぽいから」とAMSを導入すると、別の問題が出ます。
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email
has_many :posts
end
# controller
render json: @user, serializer: UserSerializer
これ自体は綺麗なんですが、
- 暗黙的な動作が多い(「has_many」がどんなSQLを発行するか見えない)
- 条件分岐が複雑になると途端に辛い(あるユーザーには特定フィールドを出したくない、など)
- メンテナンスが止まりがち(gemのissue見ると分かる)
私が参画したプロジェクトでは、AMSで複雑な権限制御をしようとして、結局Serializerクラスが500行を超える魔境になってました。
3-3. パフォーマンスの罠
どちらを使っても共通して起きるのが N+1問題 です。
# Jbuilderの例
json.users @users do |user|
json.id user.id
json.latest_post_title user.posts.order(created_at: :desc).first&.title
end
これ、「@users」が100件あったら、「user.posts」のクエリが100回走ります。
4. After:基本的な解決パターン
4-1. 使い分けの判断基準
実務では、以下のマトリクスで判断するのが確実です。
シンプルなAPI、バージョニング不要
推奨: Jbuilder
理由: Railsの標準、学習コストが低い
複数エンドポイントで共通の表現
推奨: AMS(または自作PORO)
理由: 再利用性が高い
複雑な条件分岐・権限制御
推奨: 自作PORO
理由: 明示的に書ける
GraphQL的な柔軟性が必要
推奨: 自作PORO + パラメータ
理由: AMSの暗黙制御を避ける
私の実務での結論:
- 小〜中規模API → Jbuilder + partial活用
- 複雑な権限制御あり → 自作PORベースのSerializerクラス
- AMSは「レガシーコードで既に使われている」場合のみ継続
4-2. Jbuilderのベストプラクティス
Partialで再利用する
# app/views/api/users/_user.json.jbuilder
json.id user.id
json.name user.name
json.email user.email
# app/views/api/users/index.json.jbuilder
json.users @users do |user|
json.partial! 'api/users/user', user: user
end
N+1を防ぐ
# controller
@users = User.includes(:posts).all
# view
json.users @users do |user|
json.partial! 'api/users/user', user: user
json.posts user.posts do |post|
json.partial! 'api/posts/post', post: post
end
end
4-3. ActiveModelSerializerを使う場合の注意点
明示的にincludesを指定
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email
has_many :posts
# これを忘れるとN+1
end
# controller
@users = User.includes(:posts).all
render json: @users, each_serializer: UserSerializer
条件分岐は 「if」 オプションで
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :phone
attribute :phone, if: -> { scope&.admin? }
def phone
object.phone_number
end
end