When I load up the Rails console in my project, I can do this:
{}.to_json
But I can't do this:
"{}".from_json
Even when I require the json gem first, it doesn't work. Does a from_json method even exist?
No. from_json does not exist. If you want to get a Ruby hash from a JSON string, you can use the JSON#parse method. Example below:
json = JSON.parse("{\"hello\": \"world\"}")
the above returns {"hello"=>"world"}
Your {}.to_json assumption is correct. However when we're taking JSON data (or any textual data for that matter) and converting it to some native structure we call that process parsing. An instance method of a class that parsed some textual data and initialized its attributes with that data would be odd - a bit out of place. Instead what you typically see are static factory methods (known as class methods in Ruby), like:
JSON.parse "{}"
This will return a hash in Ruby.
No, as mentiond below and above, a .from_json method does not exist. But its kinda simple to implement on you own:
require 'json'
class String
def from_json
JSON.load(self) rescue false
end
end
Just happened across this. from_json is now in the framework.
From the docs...
class Person
include ActiveModel::Model
include ActiveModel::AttributeMethods
include ActiveModel::Serializers::JSON
attr_accessor :name, :age, :awesome
def attributes=(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
def attributes
instance_values
end
end
I generally use it to serialize JSON to ActiveModel objects...
json.each do |object|
object = object.to_json
myModel = MyModel.new
myModel.from_json(object)
end
The error reporting is kinda bad with the "from_json" method, if I was dealing with malformed JSON I would get the undefined method error.
Years later, HTH!
Related
I've been working with Rails and a number of frontend js frameworks and libs, including Ember, Angular, and React. While all three libraries are powerful in their own regard, one pain point (for me at least) has always been form validation. I've always hated having to keep my model validations (in Rails) in sync with my form validations (in Ember/Angular/React).
Lately, I've been attempting to serialize a model's validators into json. However, while calling as_json on a record will give me back a json hash, it doesn't give me the type of validators for a particular attribute.
For example, let's say I have a model called Assignment. When I create a new Assignment record and call _validators on it, this is what I get.
pry(main)> Assignment.new._validators
=>
{
:title=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e123900 #attributes= [:title], #options={}>],
:full_prompt=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e122e60 #attributes=[:full_prompt], #options={}>],
:submission_window=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e1223c0 #attributes=[:submission_window], #options={}>]
}
Now here's what I get when I add on the as_json call:
pry(main)> Assignment.new._validators.as_json
=>
{
"title"=>[{"attributes"=>["title"], "options"=>{}}],
"full_prompt"=>[{"attributes"=>["full_prompt"], "options"=>{}}],
"submission_window"=>[{"attributes"=>["submission_window"], "options"=>{}}]
}
As you can see, calling as_json removes what types of validators were attached to a model's attribute.
Has anybody run into a similar situation and/or has a workaround? Thanks for any help!
Kurt
this should work better... the attributes and options hash will be a sub-hash which will be the value of a hash where the key is the validator class.. all of course themselves sub-hashes under the attributes
hash = {}
Assignment._validators.each { |k, v| v.each {|val| hash[k] ||= {}; hash[k][val.class.to_s] = val }}
hash.as_json
Rails adds a method to Object called as_json. As you can see here it checks to see if the object in question responds to to_hash (which ActiveModel::Validator does not by default).
The fallback it to invoke instance_values.as_json(options) which is how you're getting the default JSON.
I would implement the to_hash method on ActiveModel::Validator and you can put whatever information in there you'd like.
initializers/active_model_validator.rb
class ActiveModel::Validator
def to_hash
{} # custom implementation here...
end
end
If you want to maintain the default JSON and simply append additional information to it you could do something like
initializers/active_model_validator.rb
class ActiveModel::Validator
def to_hash
instance_values.tap do |hash|
# custom implementation here...
end
end
end
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.
use the built-in serialize method :
class Whatever < ActiveRecord::Base
serialize :params, HashWithIndifferentAccess
end
see ActiveRecord::Base docs on serialization for more info.
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.
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.
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.
I'm trying to access a hash by using an regular class method like this:
aHash.key
But I get this error:
undefined method `key1' for {:key1=>"value1", :key2=>"value2"}:Hash
I defined a 'method_missing' in a Hash class as suggested here:
class Hash
def method_missing(method, *params)
method = method.to_sym
return self[method] if self.keys.collect(&:to_sym).include?(method)
super
end
end
I know for sure, that when I call 'aHash.key' it does not use the Hash I defined. If I look at one on my gems, it shows this:
require "activerecord-postgres-hstore/hash"
So I checked this file, and indeed they have implemented another hash. I believe I should add 'method_missing' in there but can it be done without modified the gem?
Maybe I'm not getting how class overloading works with ruby, so the answer might be something else.
Like SporkInventor said, what you are doing is very dangerous and bad practice, you shouldn't patch Hash. Instead, like he said, use OpenStruct from Ruby's standard library. If you need, change your code to wrap the Hash into an OpenStruct when returning it.
require 'ostruct'
result = OpenStruct.new({ :a => 1, :b => 2 })
result.a # => 1
So if you really need to monkey-patch, rather monkey patch the method that is returning the hash to wrap the result in an OpenStruct, instead of patching Hash. Or if you need it to really be a Hash (obj.is_a? Hash #=> true) then create a subclass of hash with your method_missing implementation and then monkey-patch the method returning the Hash to return your OpenHash (or whatever you call it).
class OpenHash < Hash
def self.from_hash(hash)
h = OpenHash.new
h.merge!(hash)
h
end
def method_missing(method, *params)
method = method.to_sym
return self[method] if self.keys.collect(&:to_sym).include?(method)
super
end
end
For (made up) example you have a Hstore class which has a method return_hash which returns the hash. Then patch it like this:
class Hstore
def return_hash_with_ostruct
OpenStruct.new(return_hash_without_ostruct)
end
alias_method_chain :return_hash, :ostruct
end
Hstore.new.return_hash.class # => OpenStruct
Monkey-patching standard library is very bad practice!
I need to serialize a model to json and have all of the keys be camelized. I see that there's an option in to_xml to allow camel case. I can't seem to coerce the json serialization into giving me back a camelized hash. Is this something that's possible in rails?
I had a similar issue. After a bit of research I wrapped the as_json ActiveModel method with a helper that would camelize Hash keys. Then I would include the module in the relevant model(s):
# lib/camel_json.rb
module CamelJson
def as_json(options)
camelize_keys(super(options))
end
private
def camelize_keys(hash)
values = hash.map do |key, value|
[key.camelize(:lower), value]
end
Hash[values]
end
end
# app/models/post.rb
require 'camel_json'
class Post < ActiveRecord::Base
include CamelJson
end
This worked really well for our situation, which was relatively simplistic. However if you're using JBuilder, apparently there's a configuration to set camel case as the default: https://stackoverflow.com/a/23803997/251500
If you are using rails, skip the added dependency and use Hash#deep_transform_keys. It has the added benefit of also camelizing nested keys (handy if you are doing something like user.as_json(includes: :my_associated_model)):
h = {"first_name" => "Rob", "mailing_address" => {"zip_code" => "10004"}}
h.deep_transform_keys { |k| k.camelize(:lower) }
=> {"firstName"=>"Rob", "mailingAddress"=>{"zipCode"=>"10004"}}
Source: https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/core_ext/hash/keys.rb#L88
For my case,I was required to customize some key names.
Usage
puts self.camelize_array(array:Post.all.to_a,conditions:{id: "_id",post_type: "type"})
Implementation
def self.camelize_array(array:,conditions: {})
final = JSON.parse array.to_json
final.each do |a|
a.transform_keys! do |key|
if conditions.keys.include? key.to_sym
key = conditions[key.to_sym]
else
key.camelize(:lower)
end
end
end
final.to_json
end
Working with RABL Renderer directly, you can pass an inline template, instead of fetching it from a file:
Rabl::Renderer.new("\nattributes :name, :description", object).render
The \n character is necessary at the beginning of the string.
It seems weird to me to use camelized attribute names in Rails, let alone json. I would stick to the conventions and use underscored variable names.
However, have a look at this gem: RABL. It should be able to help you out.
When we try to deserialize a Model from our database we always receive a YAML object. For that we added the following code in the environment.rb:
YAML.add_domain_type("ActiveRecord,2007", "") do |type, val|
klass = type.split(":").last.constantize
YAML.object_maker(klass, val)
end
class ActiveRecord::Base
def to_yaml_type
"!ActiveRecord,2007/#{self.class}"
end
end
class ActiveRecord::Base
def to_yaml_properties
['#attributes']
end
end
This works! But only once, when I refresh the screen I always undefined method ... for YAML. It seems like my code isn't executed anymore...
Can anyone help?
Thnx!
it's not a good idea to serialize a full active record object. The object might change in the meantime and, when you load it, you might find yourself working with a stale object.
be sure the class definition of the object you are deserializing is loaded before the object is deserialized. Normally, you won't need to require the class because it's automatically loaded by Ruby when you try to use it. This doesn't happen when you deserialize an object.