3行まとめ
Rails でDBのデータを取得して json 形式に変換したい、というシーンにおいて
単に json 形式に変換したい
Rails でのアプリケーション開発において、取得してきたデータを json 形式に変換したいシーンはままあると思う。
そういう時は to_json
が便利。
例えば name に tanaken を含むユーザを取得して json 形式に変換したい時はこんな感じ。
> users = User.where('users.name like "%tanaken%"')
=> [
id: 1,
name: "tanaken0515",
email: "tanaken0515@example.com",
created_at: Wed, 05 Feb 2020 21:25:55 JST +09:00,
updated_at: Wed, 05 Feb 2020 21:28:07 JST +09:00>]
> users.to_json
=> "[{\"id\":1,\"name\":\"tanaken0515\",\"email\":\"tanaken0515@example.com\",\"created_at\":\"2020-02-05T21:25:55.000+09:00\",\"updated_at\":\"2020-02-05T21:28:07.000+09:00\"}]"
便利。
モデルの特定のカラムだけ欲しい
でもこれだと User モデルのカラムを全部出しちゃって不便なことがある。例えば「この json をフロントに渡したいんだけど email は個人情報だから渡したくない。id と name だけあれば良い。」みたいな時。
実際のお仕事(主に土日にやってるほう)で似たようなシーンがあったのだけど、先日まで to_json
を雰囲気で使っていた( json 形式に変換してくれるだけでしょ?と思っていた)自分は、「 select で絞ってから to_json
すっか〜」というアプローチをとった。
> users = User.select(:id, :name).where('users.name like "%tanaken%"')
=> [
> users.to_json
=> "[{\"id\":1,\"name\":\"tanaken0515\"}]"
そしたらレッビューで「それ to_json
に only:
オプションを指定したらできますよ〜」というコメントをいただいた。
> users = User.where('users.name like "%tanaken%"')
=> [
id: 1,
name: "tanaken0515",
email: "tanaken0515@example.com",
created_at: Wed, 05 Feb 2020 21:25:55 JST +09:00,
updated_at: Wed, 05 Feb 2020 21:28:07 JST +09:00>]
> users.to_json(only: [:id, :name])
=> "[{\"id\":1,\"name\":\"tanaken0515\"}]"
なるほど便利。ありがたレビュー、大感謝太郎。
to_json
ってなんなのよ
で、この to_json
ってなんなのよ、というお話。調べますわよ。
まずは rails console で定義元を探してみる。 source_location
が便利。
> users.method(:to_json).source_location
=> ["/path/to/project/root/vendor/bundle/ruby/2.6.0/gems/activesupport-6.0.2.2/lib/active_support/core_ext/object/json.rb", 36]
https://github.com/rails/rails/blob/9256ae8a389fd40f9e4f152737de0fb2c6059daf/activesupport/lib/active_support/core_ext/object/json.rb#L36 ここ。
to_json
は ActiveSupport::ToJsonWithActiveSupportEncoder
モジュールのメソッドだったのだなぁ。(# :nodoc:
となっているのでドキュメントはない)
実は rails console でメソッドの中身をみることもできて便利(これは最近知った).
> users.method(:to_json).source.display
def to_json(options = nil)
if options.is_a?(::JSON::State)
super(options)
else
ActiveSupport::JSON.encode(self, options)
end
end
=> nil
さっきの例で渡しているオプション only: [:id, :name]
はただの Hash なので↑の else の方に入って、ActiveSupport::JSON.encode(self, options)
が動くわけですな。
さて、続いて ActiveSupport::JSON.encode(self, options)
について調べる。何も考えずに以下を打つ。
> ActiveSupport::JSON.method(:encode).source_location
=> ["/path/to/project/root/vendor/bundle/ruby/2.6.0/gems/activesupport-6.0.2.2/lib/active_support/json/encoding.rb", 21]
> ActiveSupport::JSON.method(:encode).source.display
def self.encode(value, options = nil)
Encoding.json_encoder.new(options).encode(value)
end
=> nil
Encoding.json_encoder
がなんらかのクラスで、そのインスタンスを new して encode メソッドを呼んでる。
https://github.com/rails/rails/blob/144df5b104d042792f3fa73576d3ca9fac74fa67/activesupport/lib/active_support/json/encoding.rb#L21 この辺読むと、Encoding
はすぐ下にモジュールとして定義されていて、このモジュール内の一番下に json_encoder
が書いてある。
ざっくりこんな感じ
module Encoding
class JSONGemEncoder
end
class << self
attr_accessor :json_encoder
end
self.json_encoder = JSONGemEncoder
end
となってる。
つまり Encoding.json_encoder.new(options).encode(value)
は
Encoding::JSONGemEncoder.new(options).encode(value)
のこと。
JSONGemEncoder
クラスの中身、 encode
メソッドを見てみる。
GitHubで直接コード読めば良いんだけどあえて rails console でやるならこう。
> ActiveSupport::JSON::Encoding::JSONGemEncoder.new(only: [:id, :name]).method(:encode).source.display
def encode(value)
stringify jsonify value.as_json(options.dup)
end
=> nil
value
を as_json(options.dup)
して jsonify
したものを stringify
するよ、ということですね、直感的ですね。
value
ってなんだっけ?というと ActiveSupport::JSON.encode(self, options)
の self
で、これは詰まり users.to_json
の users
ですわ。
なので詰まるところ
> users.to_json(only: [:id, :name])
=> "[{\"id\":1,\"name\":\"tanaken0515\"}]"
> ActiveSupport::JSON::Encoding::JSONGemEncoder.new(only: [:id, :name]).encode(users)
=> "[{\"id\":1,\"name\":\"tanaken0515\"}]"
ってこと。
to_json
がなんなのかわかってすっきり。
指定できるオプションは、どこを見ればわかるのよ
to_json
の正体がわかったところで、オプションをみていこう。
value
を as_json(options.dup)
しているので、 as_json
で使えるオプションをそのまま使えるっちゅうことですな。
value.as_json
は実質 users.as_json
なので、rails console でみてみると
> users.method(:as_json).source_location
=> ["/path/to/project/root/vendor/bundle/ruby/2.6.0/gems/activerecord-6.0.2.2/lib/active_record/relation/delegation.rb", 85]
> users.method(:as_json).source.display
delegate :to_xml, :encode_with, :length, :each, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
:to_sentence, :to_formatted_s, :as_json,
:shuffle, :split, :slice, :index, :rindex, to: :records
=> nil
ほ〜ん、なるほど、 records
に delegate
されているのね。
records
は ActiveRecord::Relation#records
かな。こいつの返り値は Array
なので、結局のところ Array#as_json
を追えば良さそう。
> users.class
=> User::ActiveRecord_Relation
> users.records.class
=> Array
> users.records.method(:as_json).source_location
=> ["/path/to/project/root/vendor/bundle/ruby/2.6.0/gems/activesupport-6.0.2.2/lib/active_support/core_ext/object/json.rb", 152]
> users.records.method(:as_json).source.display
def as_json(options = nil)
map { |v| options ? v.as_json(options.dup) : v.as_json }
end
=> nil
これは Array
の各要素に対して as_json
をしているだけですな。
今回の場合 users
の各要素は User モデルのインスタンスなので
> users.first.method(:as_json).source_location
=> ["/path/to/project/root/vendor/bundle/ruby/2.6.0/gems/activemodel-6.0.2.2/lib/active_model/serializers/json.rb", 89]
> users.first.method(:as_json).source.display
def as_json(options = nil)
root = if options && options.key?(:root)
options[:root]
else
include_root_in_json
end
hash = serializable_hash(options).as_json
if root
root = model_name.element if root == true
{ root => hash }
else
hash
end
end
=> nil
なるほど、ActiveModel::Serializers::JSON
モジュールのメソッドだったのだなぁ!
そしてこれは # nodoc
ではなく、しっかりとドキュメントが書かれているのである!
api.rubyonrails.org
ここに書かれているオプションは使い放題!便便便利!
オプション紹介
ドキュメントのサンプルコードをそのまま貼って眺める
まずはオプション指定なしパターン
user = User.find(1)
user.as_json
次に :only
, :except
。良いすね。でも個人的には多分 :except
は使わないかな〜。ブラックリスト方式よりもホワイトリスト方式の方が安心できるから。センシティブな情報が入ったカラムを後から追加した時に、ブラックリストに指定し忘れるとまずいからね〜。
user.as_json(only: [:id, :name])
user.as_json(except: [:id, :created_at, :age])
続いて :methods
これ便利すな〜。
user.as_json(methods: :permalink)
最後に :include
、associationを引いてこれるのも便利だなぁ。
user.as_json(include: :posts)
3行まとめ(再)
Rails でDBのデータを取得して json 形式に変換したい、というシーンにおいて
最初に想定した分量の5倍くらいの長さになってしまった。でも勉強になったから、よしっ!