Symbols used as Hash keys get converted to Strings when serialized - ruby-on-rails

When I assign an Array or Hash to an attribute of a Mongo document, it gets properly
serialized except for Symbols when they are used as Hash keys. Simple example:
irb>MyMongoModel.create :some_attr => {:a => [:b,:c]}
=> #<MyMongoModel _id: 4d861c34c865a1f06a000001, some_attr: {:a=>[:b, :c]}>
irb>MyMongoModel.last
=> #<MyMongoModel _id: 4d861c34c865a1f06a000001, some_attr: {"a"=>[:b, :c]}>
Please, note that some_attr is retrieved as {"a"=>[:b, :c]}, not as
{:a=>[:b, :c]}
This also happens for nested Hashes (e.g., inside of Arrays or other Hashes). Is there a way to preserve Symbols in such cases?
Solution
I'm using YAML to manually serialize some_attr - YAML.dump (or Object#to_yaml) before storing, and YAML::load after reading the attribute. YAML preserves the serialized object better. ActiveRecord is using YAML to implement its serialize class method on ActiveRecord::Base.

More than likely this has to do with the ORM you are using to provide the persistance layer for the model. You can probably wrap some_attr with a method that returns it in the form of a HashWithIndifferentAccess which you can then access with either strings or arrays. Since you are using Rails, this functionality can be activated by calling the with_indifferent_access method on the Hash object. (If you have an array of Hash objects, you'll need to call it on each one of course) The method will return the same hash, but then symbol lookups will work.
From your code:
new_hash = MyMongoModel.last.some_attr.with_indifferent_access
new_hash[:a] # Will return the same as new_hash['a']
Hope this helps!

the culprit here is the BSON serialization. when you serialize a symbol used as a key for hashes, it is actually translated to a string and when you ask it back you get the string instead of the symbol.
i'm having the same problem as you and i'm thinking of extending the Hash class to include a method to convert all the "string" keys to :symbols.
unfortunately i'm not on Rails so i cannot use the with_indifferent_access as suggested by ctcherry.

I'm not sure about preserving symbols but you can convert the strings back to symbols.
str.to_sym

Found this, works well and you have define the field as Hash:
https://github.com/mindscratch/mongoid-indifferent-access

Related

How to rename a symbol in hash parameters?

I have parameters from an external API that formats JSON responses using CamelCase that I want to fit into my Rails app:
{"AccountID"=>"REAWLLY_LONG_HASH_API_KEY_FROM_EXTERNAL_SERVICE",
"ChannelProductDescription"=>"0004", "Currency"=>"CAD",
"CurrentBalance"=> {"Amount"=>"162563.64", "Currency"=>"CAD"}}
Using the below script I converted them to lower case:
data = JSON.parse(response, symbolize_keys: true)
data = {:_json => data} unless data.is_a?(Hash)
data.deep_transform_keys!(&:underscore)
data.deep_symbolize_keys!
Leaving me correctly formatted params like this:
{:account_id=>"REAWLLY_LONG_HASH_API_KEY_FROM_EXTERNAL_SERVICE",
:channel_product_description=>"SAVINGS", :currency=>"CAD",
:current_balance=> {:amount=>"43.00", :currency=>"CAD"}}
I'm trying to map this external API response into a generic Rails model Account, where JSON from this API call will return cleanly as parameters into my database to allow a clean saving interface such as:
#account = Account.create(ParamParser.call(params))
But I ran into a problem with converting :account_id, as that param conflicts with the primary key of my database.
To get around this, my idea is to convert all symbol instances of params[:account_id] into params[:account_key_id], so that those params don't conflict with my databases existing account_id field.
How do I do this, and is there a better approach for consuming external JSON API's than what I've described here?
Hash#deep_transform_keys does this:
Returns a new hash with all keys converted by the block operation.
This includes the keys from the root hash and from all
nested hashes and arrays.
So you could do it in one pass with an appropriate block, something like:
data.deep_transform_keys! do |key|
key = key.underscore.to_sym
key = :account_key_id if(key == :account_id)
key
end
You might as well drop the symbolize_keys: true flag to JSON.parse too, you're changing all the keys anyway so don't bother.
If you're doing this sort of thing a lot then you could write a method that takes a key mapping Hash and gives you a lambda for transforming the keys:
def key_mangler(key_map = { })
->(key) do
key = key.underscore.to_sym
key = key_map[key] if(key_map.has_key?(key))
key
end
end
and then say things like:
data.deep_transform_keys!(&key_mangler(:account_id => :account_key_id))
You might want to use a different name than key_mangler of course but that name is good enough to illustrate the idea.
BTW, if you're sending this JSON into the database then you probably don't need to bother with symbol keys, JSON only uses strings for keys so you'll be converting strings to symbols only for them to be converted back to strings. Of course, if you're symbolizing the keys when pulling the JSON out of the database then you'll probably want to be consistent and use symbols across the board.
In addition to the previous answer...
Unfortunately, there is, to my knowledge, no method on Hash that does this in one operation. I've always accomplished this by brute force, as in:
hash[:new_key] = hash[:old_key]
hash.delete(:old_key)
A shortcut for this, suggested in the comment below by "mu is too short", is:
hash[:new_key] = hash.delete(:old_key)
To illustrate in irb:
2.4.1 :002 > h = { foo: :bar }
=> {:foo=>:bar}
2.4.1 :003 > h[:foo_new] = h.delete(:foo)
=> :bar
2.4.1 :004 > h
=> {:foo_new=>:bar}

Select on hash with indifferent access not working

I am trying to deal with inconsistent keys (Strings/Symbols) in hashes. I thought that HashWithIndifferentAccess would be the answer but I am getting some slightly confusing results when trying to do some basic operations on these hashes
For example I have the following HashWithIndifferentAccess
(rdb:1) metadata
{"indexes"=>["respondent", "brand"], "columns"=>["rating"],
"value_labels"=>{}, "column_labels"=>{}}
(rdb:1) metadata.class
ActiveSupport::HashWithIndifferentAccess
when I try the following select I get an empty hash
(rdb:1) metadata.select{ |k, v| [:indexes, :columns, :value_labels, :column_labels]
.include? k }
{}
Are all common hash operations available with HashWithIndifferentAccess ? Why is this operation returning an empty hash
All you really get with HashWithIndifferentAccess is the ability to set and get values using either a string or a key. Once you start using other reading methods on the hash you move to objects that are not indifferent to strings or symbols.
However, HashWithIndifferentAccess does help you because:
Internally symbols are mapped to strings when used as keys in the entire writing interface (calling []=, merge, etc)
....
You are guaranteed that the key is returned as a string
This means that you're always going to get a string for keys with methods like select:
> h = { sym_key: 'sym_value', 'string_key' => 'string_value' }.with_indifferent_access
> h.keys
=> ["sym_key", "string_key"]
Indifferent access means HashWithIndifferentAccess#[] will check both strings and keys. However, there is no such patch done on Array#include?, which you are using to filter your data. Easy fix:
[:indexes, :columns, :value_labels, :column_labels].include? k.to_sym

rails using .send( ) with a serialized column to add element to hash

I'm serializing many attributes on a model Page as hashes.
Because of the high number of attributes, I've taken a meta-programming approach and want to use .send() to iterate through a collection of attributes (such that I don't have to type out an update action for each attribute.
I've done something like this:
insights.each do |ins|
self.send("#{ins.name}=", {(Time.now) => ins.values[1]['value'].to_f})
self.save
end
The problem is that this obviously overwrites the whole serialized column, whereas I wish to add this as an element to the serialized hash.
Tried something like this:
insights.each do |ins|
self.send("#{ins.name}[#{Time.now}]=", ins.values[1]['value'].to_f)
self.save
end
But get a NoMethodError: undefined method page_fan_adds_unique[Mon Aug 13 13:31:58 -0400 2012]=
In the console I'm able to do Page.find(5).page_fan_adds_unique[Time.now]= 12345 and save it as an additional element to the hash as expected.
So how can I use .send() to save an additional element to a serialized hash? Or is there some other approach? Such as using update_attribute or another method? Writing my own? Any help is appreciated, even if the advice is that I shouldn't be using serialization for this.
I'd do :
self.ins.name.send(:[]=, key, value)

Is it okay to use strings everywhere and never use symbols in Ruby on Rails?

There are some annoyances with using symbols in hashes. For example, the JSON gem that we use always returns strings from any JSON string that's parsed, so wherever we reference a hash generated from decoding JSON, we have to use a combination of strings and symbols to access hashes.
Style-wise, is it ok to keep things consistent throughout by using strings only?
Strings are mutable, hence each time you reference "foo" ruby creates a new object. You can test that by calling "foo".object_id in irb. Symbols, on the other hand, are not, so each time you reference :foo ruby returns the same object.
Regarding the "style" and "consistency" you can always use hash.symbolize_keys! for your received json data, this will turn all string keys into symbols. And vice-versa - hash.stringify_keys! to make them strings again.
There is no rule that says a hash key should be a symbol.
The symbol-as-key is seen a lot in Rails as a convention ... Rails makes a lot of use of passing hashes to allow multiple parameters, and the keys in such hashes are often symbols to indicate that they are expected/permissible parameters to a method call.
For the indecisive among us:
http://as.rubyonrails.org/classes/HashWithIndifferentAccess.html

Reusing Rails' param parsing to turn a one dimensional hash into a multideminonal one based on the key values and array notation

I am currently using rturk which give me back my answers in a one dimensional hash as such...
{"answers[125][rating]"=>"5", "answers[126][rating]"=>"5", "commit"=>"Take Survey", "answers[125][rating]"=>"5", "authenticity_token"=>"je0Hx48qKmCzy1zmXCpijYWbl4w92eDMRajWJcVYxe0=", "gender"=>"m", "answers[120][rating]"=>"5", "answers[121][rating]"=>"5", "income"=>"$75,000 to $100,000", "answers[122][rating]"=>"5", "date[year]"=>"1992", "career"=>"Marketer", "answers[123][rating]"=>"5", "answers[124][rating]"=>"5"}
What I would like to do is parse those into a multidimensional hash that I can then just pass as attributes. If I can figure this out I'd probably switch from form_tag back to rails stand object forms.
Yes, Rack::Utils.parse_nested_query could help you, but you need to convert hash to string:
h = {"answers[125][rating]"=>"5", ...} # your hash
s = h.to_a.collect { |a| a.join('=') }.join('&')
Rack::Utils.parse_nested_query(s)
Take a look at the Turkee gem (http://github.com/aantix/turkee) as it will do this object mapping for you.

Resources