3行まとめ
Rails でDBのデータを取得して json 形式に変換したい、というシーンにおいて
to_json
はActiveSupport::ToJsonWithActiveSupportEncoder
のメソッドto_json
にはas_json
(ActiveModel::Serializers::JSON) のオプションが渡せるas_json
のオプションはとても便利
単に json 形式に変換したい
Rails でのアプリケーション開発において、取得してきたデータを json 形式に変換したいシーンはままあると思う。
そういう時は to_json
が便利。
例えば name に tanaken を含むユーザを取得して json 形式に変換したい時はこんな感じ。
> users = User.where('users.name like "%tanaken%"') => [#<User:0x00007faead5daf08 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%"') => [#<User:0x00007faead64f5b0 id: 1, name: "tanaken0515">] > users.to_json => "[{\"id\":1,\"name\":\"tanaken0515\"}]"
そしたらレッビューで「それ to_json
に only:
オプションを指定したらできますよ〜」というコメントをいただいた。
> users = User.where('users.name like "%tanaken%"') => [#<User:0x00007faead5daf08 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]
to_json
は ActiveSupport::ToJsonWithActiveSupportEncoder
モジュールのメソッドだったのだなぁ。(# :nodoc:
となっているのでドキュメントはない)
実は rails console でメソッドの中身をみることもできて便利(これは最近知った).
> users.method(:to_json).source.display def to_json(options = nil) if options.is_a?(::JSON::State) # Called from JSON.{generate,dump}, forward it to JSON gem's to_json super(options) else # to_json is being invoked directly, use ActiveSupport's encoder 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 #:nodoc: class JSONGemEncoder #:nodoc: # 略 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) #:nodoc: 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
ではなく、しっかりとドキュメントが書かれているのである!
ここに書かれているオプションは使い放題!便便便利!
オプション紹介
ドキュメントのサンプルコードをそのまま貼って眺める
まずはオプション指定なしパターン
user = User.find(1) user.as_json # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
次に :only
, :except
。良いすね。でも個人的には多分 :except
は使わないかな〜。ブラックリスト方式よりもホワイトリスト方式の方が安心できるから。センシティブな情報が入ったカラムを後から追加した時に、ブラックリストに指定し忘れるとまずいからね〜。
user.as_json(only: [:id, :name]) # => { "id" => 1, "name" => "Konata Izumi" } user.as_json(except: [:id, :created_at, :age]) # => { "name" => "Konata Izumi", "awesome" => true }
続いて :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" }
最後に :include
、associationを引いてこれるのも便利だなぁ。
user.as_json(include: :posts) # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" }, # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
3行まとめ(再)
Rails でDBのデータを取得して json 形式に変換したい、というシーンにおいて
to_json
はActiveSupport::ToJsonWithActiveSupportEncoder
のメソッドto_json
にはas_json
(ActiveModel::Serializers::JSON) のオプションが渡せるas_json
のオプションはとても便利
最初に想定した分量の5倍くらいの長さになってしまった。でも勉強になったから、よしっ!