Rails APIモードでレスポンスのハッシュに任意の値を持たせつつネストさせたい時の注意点
やりたいこと
- テーブルのカラム以外にインスタンスメソッドの返り値もレスポンスに含めたい
- レスポンスのデータはネストした構造にしたい
// GET: /api/v1/ideas のレスポンス
{
// dataに配列が入っている構造
"data": [
{
"id": 1,
"body": "Go言語の本を読む",
// これが idea.rb に定義したメソッドの返り値と想定
"category_name": "勉強"
}
// ...
]
}
前提知識
as_json
と to_json
as_json
や to_json
を使うと ActiveRecord_Relation
を Hash の配列に変換できます。用意されているオプションなどはほぼ同じなのですが、as_json
と to_json
には以下のような違いがあります。
as_json
: Hash を返すto_json
: String を返す
> User.first.as_json
=> Hash
> User.first.to_json
=> String
methods
オプションを指定することで「インスタンスメソッドの返り値もレスポンスに含めたい」の要件は実装できます。
user.as_json(methods: :permalink)
# => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
# "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# "permalink" => "1-konata-izumi" }
ActiveModel::Serializers::JSON
to_json で実装しているとぶつかる壁
まずはto_json
を使わず、シンプルな実装をしてみます。この実装ではまずレスポンスのデータが想定している構造で問題なく出力されます。
def index
render json: { data: Idea.order(:id) }
end
// curl localhost:3000 | json_pp を実行
{
"data": [
{
"body": "Go言語の本を読む",
"id": 1,
"created_at": "2021-03-20T02:33:36.893Z",
"category_id": 1,
"updated_at": "2021-03-20T02:33:36.893Z"
},
{
"created_at": "2021-03-20T02:33:36.916Z",
"category_id": 1,
"updated_at": "2021-03-20T02:33:36.916Z",
"body": "Ruby on RailsでAPI開発",
"id": 2
}
]
}
上記ではto_json
を呼び出していませんが、Idea.order(:id)
を Rails がよしなにハッシュの配列に変換してくれています。
出力するオブジェクトに対して to_json を呼び出す必要はありません。:json オプションが指定されていれば、render によって to_json が自動的に呼び出されるようになっています。
レイアウトとレンダリング - Rails ガイド
「テーブルのカラム以外にインスタンスメソッドの返り値もレスポンスに含めたい」の要件を実装するためto_json
がオプションとして用意している、methods
を使ってみます。しかし、下記の実装では data の値が文字列になってしまいます。
def index
render json: { data: Idea.order(:id).to_json(methods: [:category_name]) }
end
// curl localhost:3000 | json_pp
{
"data": "[{\"id\":1,\"body\":\"Go言語の本を読む\",\"category_id\":1,\"created_at\":\"2021-03-20T02:33:36.893Z\",\"updated_at\":\"2021-03-20T02:33:36.893Z\",\"category_name\":\"勉強\"}, ...]"
}
原因
状況を整理するため、data のネストを剥がして、methods オプションなしでto_json
を使ってみます。こうすると、先程のような文字列にはならず綺麗に整形されたデータを返します。
def index
render json: Idea.order(:id).to_json
end
// curl localhost:3000 | json_pp
[
{
"id": 1,
"body": "Go言語の本を読む",
"category_id": 1,
"created_at": "2021-03-20T02:33:36.893Z",
"updated_at": "2021-03-20T02:33:36.893Z"
},
{
"body": "Ruby on RailsでAPI開発",
"id": 2,
"created_at": "2021-03-20T02:33:36.916Z",
"updated_at": "2021-03-20T02:33:36.916Z",
"category_id": 1
}
]
json
に Hash が渡された時の挙動も確かめてみます。
def index
render json: { data: 'hoge' }
end
{
"data": "hoge"
}
なので状況を整理すると、以下のようになります。
json
で指定するオブジェクトに対してto_json
を呼び出す必要はないjson
で指定するオブジェクトがto_json
されて文字列の場合、それを整形して返すjson
で指定するオブジェクトが Hash の場合は、それをそのまま返す
先程のこのコードの場合 { data: 'to_jsonによって生成された文字列' }
という Hash が渡されたことになるので、整形してくれません。
def index
render json: { data: Idea.order(:id).to_json(methods: [:category_name]) }
end
解決策
今回の場合は as_json
を使って Hash の配列を data に渡す必要があります。
def index
render json: { data: Idea.order(:id).as_json(methods: [:category_name]) }
end