シリアライゼーション — レスポンスの形を整える
「このJSON、使いづらい」
iOSエンジニアの林さんからSlackにメッセージが届いた。
「ナツミさん、APIのレスポンスなんですが……created_at が "2024-01-15T09:23:45.123Z" で来てるんですけど、アプリ側でパース大変で。あと user_id はIDだけじゃなくてユーザー名も欲しいです。それと、物件の写真URLは配列で来てますか?」
ナツミは気づいた。render json: @property はActive Recordのattributesをそのまま垂れ流すだけ。本番で必要なレスポンス設計はもっと丁寧に行う必要がある。
# この実装では全フィールドが漏れる
def show
@property = Property.find(params[:id])
render json: @property # 危険!password_digestも含まれる可能性
endシリアライゼーションの選択肢
Railsには複数のシリアライゼーション手段がある。
jbuilder — テンプレートベースのアプローチ
jbuilderはRailsにデフォルトで含まれるgemで、ERBテンプレートと同じ感覚でJSONを組み立てられる。
インストール
# Gemfile(Rails標準で含まれている)
gem 'jbuilder'実装例
# app/views/api/v1/properties/show.json.jbuilder
json.data do
json.id @property.id
json.name @property.name
json.price @property.price
json.area @property.area
json.location do
json.prefecture @property.prefecture
json.city @property.city
json.full_address "#{@property.prefecture}#{@property.city}#{@property.address}"
end
json.owner do
json.id @property.user.id
json.name @property.user.name
json.avatar_url @property.user.avatar_url
end
json.photos @property.photos do |photo|
json.id photo.id
json.url photo.url
json.thumbnail_url photo.thumbnail_url
end
json.published_at @property.published_at&.strftime('%Y-%m-%d')
json.created_at @property.created_at.iso8601
end# app/views/api/v1/properties/index.json.jbuilder
json.data @properties do |property|
json.partial! 'api/v1/properties/property', property: property
end
json.meta do
json.current_page @properties.current_page
json.total_pages @properties.total_pages
json.total_count @properties.total_count
end# app/views/api/v1/properties/_property.json.jbuilder
json.id property.id
json.name property.name
json.price property.price
json.area property.area
json.thumbnail_url property.photos.first&.thumbnail_url
json.created_at property.created_at.iso8601INFO
jbuilderのメリット:学習コストが低く、RailsのViewレイヤーと統一感がある。部分テンプレートで再利用しやすい。N+1クエリが発生しやすいので includes を忘れずに。
ActiveModelSerializers — クラスベースのアプローチ
ViewではなくSerializerクラスにシリアライゼーションロジックを集約するアプローチ。
インストール
# Gemfile
gem 'active_model_serializers'Serializerクラスの実装
# app/serializers/property_serializer.rb
class PropertySerializer < ActiveModel::Serializer
attributes :id, :name, :price, :area, :published_at, :created_at
belongs_to :user, serializer: UserSerializer
has_many :photos, serializer: PhotoSerializer
attribute :location do
{
prefecture: object.prefecture,
city: object.city,
full_address: "#{object.prefecture}#{object.city}#{object.address}"
}
end
attribute :published_at do
object.published_at&.strftime('%Y-%m-%d')
end
attribute :created_at do
object.created_at.iso8601
end
end
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :avatar_url
# password_digestなどは含めない
end
# app/serializers/photo_serializer.rb
class PhotoSerializer < ActiveModel::Serializer
attributes :id, :url, :thumbnail_url
endコントローラでの使い方
def show
@property = Property.includes(:user, :photos).find(params[:id])
render json: @property, serializer: PropertySerializer
end
def index
@properties = Property.includes(:user, :photos).published
.page(params[:page]).per(20)
render json: @properties,
each_serializer: PropertySerializer,
meta: pagination_meta(@properties),
adapter: :json
endjsonapi-serializer — JSON:API準拠
JSON:APIは標準化されたJSONフォーマット仕様。大規模なAPIや外部公開APIに向いている。
JSON:APIのレスポンス構造
{
"data": {
"id": "1",
"type": "properties",
"attributes": {
"name": "渋谷マンション",
"price": 150000,
"area": 35.5
},
"relationships": {
"user": {
"data": { "id": "42", "type": "users" }
}
}
},
"included": [
{
"id": "42",
"type": "users",
"attributes": {
"name": "田中太郎"
}
}
]
}インストールと実装
# Gemfile
gem 'jsonapi-serializer'# app/serializers/property_serializer.rb
class PropertySerializer
include JSONAPI::Serializer
set_type :properties
set_id :id
attributes :name, :price, :area
attribute :location do |property|
{
prefecture: property.prefecture,
city: property.city
}
end
attribute :published_at do |property|
property.published_at&.strftime('%Y-%m-%d')
end
belongs_to :user
has_many :photos
end# コントローラ
def show
@property = Property.includes(:user, :photos).find(params[:id])
render json: PropertySerializer.new(@property, include: [:user, :photos]).serializable_hash
endWARNING
JSON:APIはオーバーエンジニアリングになりやすい。チームやクライアントがJSON:API仕様を理解していないと、構造が複雑すぎて使いにくい。社内APIや小規模なAPIにはシンプルなJSONで十分な場合が多い。
N+1クエリ問題の解決
シリアライゼーションで最も注意すべきはN+1クエリ。
# N+1が発生するコード
Property.all.each do |property|
property.user.name # ← userをN回クエリ
property.photos.count # ← photosをN回クエリ
end-- SQLログ(N+1の証拠)
SELECT * FROM properties;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
-- ... N件分繰り返される# includesで解決
@properties = Property
.includes(:user, :photos)
.where(published: true)
.page(params[:page])
# これで2クエリに
# SELECT * FROM properties WHERE ...
# SELECT * FROM users WHERE id IN (1, 2, 3, ...)
# SELECT * FROM photos WHERE property_id IN (1, 2, 3, ...)INFO
Bullet gemでN+1クエリを自動検出できる。開発環境でのみ有効にして、N+1を見つけたら includes で解消する。
# Gemfile
group :development do
gem 'bullet'
end
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.rails_logger = true
Bullet.add_footer = true
endどのシリアライザーを選ぶか
ナツミのチームは最終的にjsonapi-serializerを選んだ。理由は以下の通り。
| 観点 | jbuilder | AMS | jsonapi-serializer |
|---|---|---|---|
| 学習コスト | 低 | 中 | 中 |
| テストしやすさ | △(View依存) | ○ | ○ |
| パフォーマンス | △ | △ | ○ |
| 標準化 | ✗ | ✗ | ○(JSON:API) |
| 外部公開API | △ | △ | ○ |
# 最終実装:コントローラがスッキリ
module Api
module V1
class PropertiesController < Api::V1::BaseController
def index
@properties = Property.includes(:user, :photos)
.published
.page(params[:page]).per(20)
render json: PropertySerializer.new(
@properties,
include: [:user, :photos],
meta: pagination_meta(@properties),
params: { current_user: current_user }
).serializable_hash
end
def show
@property = Property.includes(:user, :photos).find(params[:id])
render json: PropertySerializer.new(
@property,
include: [:user, :photos]
).serializable_hash
end
end
end
endまとめ
render json: @modelは危険。センシティブなフィールドが漏れる可能性がある- jbuilder:学習コスト低、テンプレートで直感的。小規模プロジェクト向け
- ActiveModelSerializers:クラスベースで整理しやすい。中規模プロジェクト向け
- jsonapi-serializer:JSON:API標準準拠、テストしやすい。外部公開APIや大規模向け
- N+1クエリは
includesで解決し、Bullet gemで自動検出する
次章では、「APIを使えるのは誰か」を制御する認証とアクセス制御を学ぶ。