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.
Related
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.
This issue will surface for many who depend on Ruby's JSON serialization outside of a Rails projects. When they try to use their code in a Rails project, it will not work as expected.
The following code run from Ruby (no Rails), prints A.
When run from rails console, it prints Hash.
That means my json serialization works in my command line lib/app, but not when it's imported into a Rails project.
What is the reason/workaround for this?
require 'json'
class A
def to_json(*a)
{:json_class => self.class.name}.to_json(*a)
end
def self.json_create(o)
A.new
end
end
class B
attr_accessor :value
def initialize(value)
#value = value
end
def to_json(*a)
{:json_class => self.class.name, :value => value}.to_json(*a)
end
def self.json_create(o)
B.new(o['value'])
end
end
b = JSON.parse(B.new(A.new).to_json)
puts b.value.class
Ruby is 1.9.3, Rails is 3.2.10
The problem is that Rails uses ActiveSupport::JSON.
For serializing, it uses as_json, not to_json. So the line
{:json_class => self.class.name, :value => value}.to_json(*a)
does not include a JSON version of value in the hash because Class A does not have a as_json method. To get your code to work the same in both Ruby and Rails, you need to explicitly call your A::to_json and A::json_create methods, like this:
def to_json(*a)
{:json_class => self.class.name, :value => JSON.dump(value)}.to_json(*a)
end
def self.json_create(o)
B.new(A.json_create(o['value']))
end
Then call, b = JSON.parse(JSON.dump(B.new(A.new)))
This wlll fix the example, but I think you may want to read this explanation of to_json vs as_json and revise your code appropriately.
According to others, the answer is yes.
http://www.rubyhood.com/2011/06/rails-spoiled-standard-json-library.html
In short, make as_json do what to_json does. That got me what I wanted/expected (and what I've been getting from pure Ruby - Rails).
For those still wandering why the strange behavior is occurring in rails the explanation can be found in:
https://github.com/flori/json/compare/v1.6.7...v1.6.8
and
https://github.com/intridea/multi_json/compare/v1.5.0...v1.5.1
Since in these version upgrades JSON.parse works different. JSON.load might still be helpful. The fastest fix would be:
gem 'json', '1.6.7'
gem 'multi_json', '1.5.0'
but leave some security issues open. Explicitly supplying create_additions: true to JSON parse when needed is recommended.
An active_resource based class:
Contact.search(:email => ['bar#foo.com','foo#bar.com'])
would produce this:
?email[]=bar#foo.com&email[]=foo#bar.com
The specific API that I'm working with requires this:
?email=bar#foo.com&email=foo#bar.com
So playing around I have found that:
ActiveResouce calls:
# Find every resource
find_every(options)
which calls:
# Builds the query string for the request.
def query_string(options)
"?#{options.to_query}" unless options.nil? || options.empty?
end
So if I update:
class Array
# Converts an array into a string suitable for use as a URL query string,
# using the given +key+ as the param name.
#
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
def to_query(key)
prefix = "#{key}[]"
collect { |value| value.to_query(prefix) }.join '&'
end
end
to this:
class Array
# Converts an array into a string suitable for use as a URL query string,
# using the given +key+ as the param name.
#
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
def to_query(key)
prefix = "#{key}"
collect { |value| value.to_query(prefix) }.join '&'
end
end
it works!! however I'm not particularly happy redefining Array.to_param because this may have unforeseen issues, especially as this plug in needs to work within rails.
Is there another way I can patch only my version?
I would definitely recommend NOT monkey patching an array method like that. If you only have a single model, could you override the search method?
class Contact
def self.search(options={})
super(options).gsub('[]','')
end
end
As this behaviour is standard through the API I'm using I was able to add this patch to my ActiveRecord::Base class.
def query_string(options)
begin
super(options).gsub('%5B%5D','')
rescue
end
end
Thanks to Beerlington for pointing me in the right direction for this.
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!
So this is a bit of a silly one and is more lack of programming knowledge rather than anything ruby or rails specific.
If i wanted to turn an ordinary class into hash its not so bad. I already have one:
class CustomRequest
require 'json'
require 'net/http'
attr_accessor :url, :depth, :status
def initialize(url,depth)
#url = url
#depth = depth
end
def make_me_hash_and_send
#myReq = {:url => #url, :depth =>#depth}
myJsonReq = #myReq
puts myJsonReq
res = Net::HTTP.post_form(URI.parse('http://127.0.0.1:3008/user_requests/add.json'),
myJsonReq)
end
end
Simply generates the hash from the internal variables that are passed in the constructor. I want to do the same for active record but the abstractness of it isn't making it easy.
Lets say I have this class
def turn_my_insides_to_hash
#How do I take the actual record variables
# nd generate a hash from them.
#Is it something like
#myHash = {:myString = self.myString
:myInt => self.myInt }
end
I may be looking at this the wrong way. I know Outside of the class I could simply say
#x = Result.find(passed_id).to_hash
and then do what I want to it. But I would rather call something liks
#x = Result.send
(which turns the result's variables into hash and sends them)
I already have the send part, just need to know how to turn variables into hash from inside class.
You could try use JSON instead of YAML:
Result.find(passed_id).to_json
or
Result.find(passed_id).attributes.to_json
also you can use options like :except and :only for to_json method.
Result.find(passed_id).attributes.to_json(:only => ['status', 'message'])
record.serializable_hash
http://api.rubyonrails.org/v4.0.12/classes/ActiveModel/Serialization.html#method-i-serializable_hash
I write something more because SO ask me to do so.