22

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.

7
  • 1
    model.params.merge!(test: 'test').save ? Commented Jan 28, 2013 at 18:48
  • No, I don't want to have to save to the database. This is mainly used for testing purposes for now. The only other workaround I've found is model.params = model.params.merge(test: 'test'). Commented Jan 28, 2013 at 18:50
  • I was hoping there might be some ruby magic, like a special method I can add for params, something like params{}. Commented Jan 28, 2013 at 18:50
  • model.params[:test] = 'test' by any chance? Commented Jan 28, 2013 at 18:52
  • variable assignment still works, as I illustrated with model.params = ..., it is Hash.merge I cannot perform directly on the attribute, even though it returns the updated hash. Commented Jan 28, 2013 at 18:56

4 Answers 4

19

use the built-in serialize method :

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

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

Sign up to request clarification or add additional context in comments.

9 Comments

This means I have to feed it a hash with indifferent access. I just want to return one. Thanks, though. This seems like the right direction.
Researched the same problem just last night... since you can pass any class to serialize that has load and dump methods, I just wrapped the JSON with my own class, JSONWithSymbolizedNames, passing in symbolize_names:true.
I having an error in Rails 4 when I initialise the attribute with a Hash and try to save the instance: Attribute was supposed to be a ActiveSupport::HashWithIndifferentAccess, but was a Hash.
@fguillen - posted... hope it helps. And apologies for misspelling your name first time around ;)
This doesn't work if the serialized value is a array of hashes
|
8

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.

Comments

6

Using HashWithIndifferentAccess is great, but it still acts like a Hash, and it can only serialize as YAML in the database.

My preference, using Postgres 9.3 and higher, is to use the json column type in Postgres. This means that when the table is read, ActiveRecord will get a Hash directly from Postgres.

create_table "gadgets" do |t|
  t.json "info"
end

ActiveRecord serialize requires that you provide it a single class that is both responsible for reading/writing the data and serializing/deserializing it.

So you can create an object that does the job by inheriting from HashWithIndifferentAccess, or my preference, Hashie::Mash. Then you implement the serialization as the dump and load class methods.

class HashieMashStoredAsJson < Hashie::Mash
  def self.dump(obj)
    ActiveSupport::JSON.encode(obj.to_h)
  end


  def self.load(raw_hash)
    new(raw_hash || {}) 
  end
end

In your model, you can specify this class for serialization.

class Gadget < ActiveRecord::Base
  serialize :info, HashieMashStoredAsJson

  # This allows the field to be set as a Hash or anything compatible with it.
  def info=(new_value)
    self[:info] = HashieMashStoredAsJson.new new_value
  end
end

If you don't use the json column type in Postgres, the implementation changes slightly

Full code and documentation here: using a JSON column type and using a string column type.

Comments

6

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.

2 Comments

Perfect! This is exactly the kind of thing I was just about to write if there wasn't an off the peg solution.
Just copied the class and used serialize :params, JsonWithIndifferentAccess and worked like a charm!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.