Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails return JSON serialized attribute with_indifferent_access

I previously had:

serialize :params, JSON

But this would return the JSON and convert hash key symbols to strings. I want to reference the hash using symbols, as is most common when working with hashes. I feed it symbols, Rails returns strings. To avoid this, I created my own getter/setter. The setter is simple enough (JSON encode), the getter is:

  def params
    read_attribute(:params) || JSON.parse(read_attribute(:params).to_json).with_indifferent_access
  end

I couldn't reference params directly because that would cause a loop, so I'm using read_attribute, and now my hash keys can be referenced with symbols or strings. However, this does not update the hash:

model.params.merge!(test: 'test')
puts model.params # => returns default params without merge

Which makes me think the hash is being referenced by copy.

My question is twofold. Can I extend active record JSON serialization to return indifferent access hash (or not convert symbols to strings), and still have hash work as above with merge? If not, what can I do to improve my getter so that model.params.merge! works?

I was hoping for something along the lines of (which works):

  def params_merge!(hash)
    write_attribute(:params, read_attribute(:params).merge(hash))
  end

  # usage: model.params_merge!(test: 'test')

Better yet, just get Rails to return a hash with indifferent access or not convert my symbols into strings! Appreciate any help.

like image 924
Damien Roche Avatar asked Jan 28 '13 18:01

Damien Roche


3 Answers

use the built-in serialize method :

class Whatever < ActiveRecord::Base
 serialize :params, HashWithIndifferentAccess
end

see ActiveRecord::Base docs on serialization for more info.

like image 142
m_x Avatar answered Nov 05 '22 02:11

m_x


Posting comment as answer, per @fguillen's request... Caveat: I am not typically a Rubyist… so this may not be idiomatic or efficient. Functionally, it got me what I wanted. Seems to work in Rails 3.2 and 4.0...

In application_helper.rb:

module ApplicationHelper
  class JSONWithIndifferentAccess
    def self.load(str)
      obj = HashWithIndifferentAccess.new(JSON.load(str))
      #...or simply: obj = JSON.load(str, nil, symbolize_names:true)
      obj.freeze #i also want it set all or nothing, not piecemeal; ymmv
      obj
    end
    def self.dump(obj)
      JSON.dump(obj)
    end
  end
end

In my model, I have a field called rule_spec, serialized into a text field:

serialize :rule_spec, ApplicationHelper::JSONWithIndifferentAccess

Ultimately, I realized I just wanted symbols, not indifferent access, but by tweaking the load method you can get either behavior.

like image 35
bimsapi Avatar answered Nov 05 '22 04:11

bimsapi


I ended up using a variation on bimsapi's solution that you can use not only with simple un-nested JSON but any JSON.

Once this is loaded...

module JsonHelper

  class JsonWithIndifferentAccess
    def self.load(str)
      self.indifferent_access JSON.load(str)
    end

    def self.dump(obj)
      JSON.dump(obj)
    end

    private

      def self.indifferent_access(obj)
        if obj.is_a? Array
          obj.map!{|o| self.indifferent_access(o)}
        elsif obj.is_a? Hash
          obj.with_indifferent_access
        else
          obj
        end
      end
  end

end

then instead of calling

JSON.load(http_response)

you just call

JsonHelper::JsonWithIndifferentAccess.load(http_response)

Does the same thing but all the nested hashes are indifferent access.

Should serve you well but think a little before making it your default approach for all parsing as massive JSON payloads will add significant ruby operations on top of the native JSON parser which is optimised in C and more fully designed for performance.

like image 27
Luke Ehresman Avatar answered Nov 05 '22 02:11

Luke Ehresman