I have a model that uses a serialized column:
class Form < ActiveRecord::Base
serialize :options, Hash
end
Is there a way to make this serialization use JSON instead of YAML?
In Rails 3.1 you can just
class Form < ActiveRecord::Base
serialize :column, JSON
end
In Rails 3.1 you can use custom coders with serialize.
class ColorCoder
# Called to deserialize data to ruby object.
def load(data)
end
# Called to convert from ruby object to serialized data.
def dump(obj)
end
end
class Fruits < ActiveRecord::Base
serialize :color, ColorCoder.new
end
Hope this helps.
References:
Definition of serialize:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556
The default YAML coder that ships with rails:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb
And this is where the call to the load happens:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132
Update
See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.
Probably this is what you're looking for.
Form.find(:first).to_json
Update
1) Install 'json' gem:
gem install json
2) Create JsonWrapper class
# lib/json_wrapper.rb
require 'json'
class JsonWrapper
def initialize(attribute)
#attribute = attribute.to_s
end
def before_save(record)
record.send("#{#attribute}=", JsonWrapper.encrypt(record.send("#{#attribute}")))
end
def after_save(record)
record.send("#{#attribute}=", JsonWrapper.decrypt(record.send("#{#attribute}")))
end
def self.encrypt(value)
value.to_json
end
def self.decrypt(value)
JSON.parse(value) rescue value
end
end
3) Add model callbacks:
#app/models/user.rb
class User < ActiveRecord::Base
before_save JsonWrapper.new( :name )
after_save JsonWrapper.new( :name )
def after_find
self.name = JsonWrapper.decrypt self.name
end
end
4) Test it!
User.create :name => {"a"=>"b", "c"=>["d", "e"]}
PS:
It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.
My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:
require "json/ext"
before_save :json_serialize
after_save :json_deserialize
def json_serialize
self.options = self.options.to_json
end
def json_deserialize
self.options = JSON.parse(options)
end
def after_find
json_deserialize
end
Cheers, quite easy in the end!
The serialize :attr, JSON using composed_of method works like this:
composed_of :auth,
:class_name => 'ActiveSupport::JSON',
:mapping => %w(url to_json),
:constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }
where url is the attribute to be serialized using json
and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)
I wrote my own YAML coder, that takes a default. Here is the class:
class JSONColumn
def initialize(default={})
#default = default
end
# this might be the database default and we should plan for empty strings or nils
def load(s)
s.present? ? JSON.load(s) : #default.clone
end
# this should only be nil or an object that serializes to JSON (like a hash or array)
def dump(o)
JSON.dump(o || #default)
end
end
Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:
class Person < ActiveRecord::Base
validate :name, :pets, :presence => true
serialize :pets, JSONColumn.new([])
end
I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.
A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.
Here is the gist of it:
composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
:constructor => Settings.method(:from_json),
:converter => Settings.method(:from_json)
after_validation do |u|
u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.
Related
I have the following class:
class Profile < ActiveRecord::Base
serialize :data
end
Profile has a single column data that holds a serialized hash. I would like to define accessors into that hash such that I can execute profile.name instead of profile.data['name']. Is that possible in Rails?
The simple straightforward way:
class Profile < ActiveRecord::Base
serialize :data
def name
self.data['name']
end
def some_other_attribute
self.data['some_other_attribute']
end
end
You can see how that can quickly become cumbersome if you have lots of attributes within the data hash that you want to access.
So here's a more dynamic way to do it and it would work for any such top level attribute you want to access within data:
class Profile < ActiveRecord::Base
serialize :data
def method_missing(attribute, *args, &block)
return super unless self.data.key? attribute
self.data.fetch(attribute)
end
# good practice to extend respond_to? when using method_missing
def respond_to?(attribute, include_private = false)
super || self.data.key?(attribute)
end
end
With the latter approach you can just define method_missing and then call any attribute on #profile that is a key within data. So calling #profile.name would go through method_missing and grab the value from self.data['name']. This will work for whatever keys are present in self.data. Hope that helps.
Further reading:
http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html
http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
def name
data[:name] if data
end
end
I'm going to answer my own question. It looks like ActiveRecord::Store is what I want:
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
So my class would become:
class Profile < ActiveRecord::Base
store :data, accessors: [:name], coder: JSON
end
I'm sure everyone else's solutions work just fine, but this is so clean.
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
["name", "attr2", "attr3"].each do |method|
define_method(method) do
data[method.to_sym] if data
end
end
end
Ruby is extremely flexible and your model is just a Ruby Class. Define the "accessor" method you want and the output you desire.
class Profile < ActiveRecord::Base
serialize :data
def name
data['name'] if data
end
end
However, that approach is going to lead to a lot of repeated code. Ruby's metaprogramming features can help you solve that problem.
If every profile contains the same data structure you can use define_method
[:name, :age, :location, :email].each do |method|
define_method method do
data[method] if data
end
end
If the profile contains unique information you can use method_missing to attempt to look into the hash.
def method_missing(method, *args, &block)
if data && data.has_key?(method)
data[method]
else
super
end
end
I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.
Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.
Here's the json in the AngularJS front-end for the candidate:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}
Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}
The candidate_controller.rb contains a simple update:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def update
respond_with Candidate.update(params[:id], candidate_params)
end
private
def candidate_params
params.require(:candidate).permit(:nickname,
works_attributes: [:id, :title, :description])
end
end
The candidate.rb model includes the following code defining the has_many relationship with the works model:
class Candidate < ActiveRecord::Base
## Model Relationships
belongs_to :users
has_many :works, :dependent => :destroy
## Nested model attributes
accepts_nested_attributes_for :works, allow_destroy: true
## Validations
validates_presence_of :nickname
validates_uniqueness_of :user_id
end
And finally, the works.rb model defines the other side of the has_many relationship:
class Work < ActiveRecord::Base
belongs_to :candidate
end
I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.
Thanks!
I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:
class CandidatesController < ApplicationController
respond_to :json
nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
format: :json
# ...
end
Refer to this issue in Rails to see if/when they fix this problem.
Update 10/17
Pending a PR merge here: rails/rails#19254.
I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:
"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."
Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
wrap_parameters include: [:id, :nickname, :works_attributes]
...
Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails
You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:
module ActionController
module ParamsWrapper
Options.class_eval do
def include
return super if #include_set
m = model
synchronize do
return super if #include_set
#include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
self.include = m.attribute_names + nested_attributes_names_array_of(m)
end
end
end
end
private
# added method. by default code was equivalent to this equaling to []
def nested_attributes_names_array_of model
model.nested_attributes_options.keys.map { |nested_attribute_name|
nested_attribute_name.to_s + '_attributes'
}
end
end
end
end
first of all, i am using rails 3.1.3 and carrierwave from the master
branch of the github repo.
i use a after_init hook to determine fields based on an attribute of
the page model instance and define attribute accessors for these field
which store the values in a serialized hash (hope it's clear what i am
talking about). here is a stripped down version of what i am doing:
class Page < ActiveRecord::Base
serialize :fields, Hash
after_initialize :set_accessors
def set_accessors
case self.template
when 'standard'
class << self
define_method 'image' do
self.fields['image']
end
define_method 'image=' do |value|
self.fields['image'] = value
end
end
mount_uploader :image, PageImageUploader
end
end
end
end
leaving out the mount_uploader command gives me access to the
attribute as i want. but when i mount the uploader a get an error
message saying 'undefined method new for nil class'
i read in the source that there are the methods read_uploader and
write_uploader in the extensions module.
how do i have to override these to make the mount_uploader command
work with my 'virtual' attribute.
i hope somebody has an idea how i can solve this problem. thanks a lot
for your help.
best regard. dominik.
Same problem but solved in your model you should override read_uploader(column) and write_uploader(column, identifier) instance methods. I also have a problem with #{column}_will_change! and #{column}_changed? for a virtual column so I had to define them too:
class A < ActiveRecord::Base
serialize :meta, Hash
mount_uploader :image, ImageUploader
def image_will_change!
meta_will_change!
#image_changed = true
end
def image_changed?
#image_changed
end
def write_uploader(column, identifier)
self.meta[column.to_s] = identifier
end
def read_uploader(column)
self.meta[column.to_s]
end
end
Now there's also an add-on to carrierwave which provides the exact functionality as described by Antiarchitect:
https://github.com/timsly/carrierwave-serializable
I have a model with data that should never be included when it is rendered as json. So I implemented the class' as_json method to behave appropriately. The problem is when other models with associations with this model render json, my custom as_json is not being called.
class Owner < ActiveRecord::Base
has_one :dog
def as_json(options={})
puts "Owner::as_json"
super(options)
end
end
class Dog < ActiveRecord::Base
belongs_to :owner
def as_json(options={})
puts "Dog::as_json"
options[:except] = :secret
super(options)
end
end
Loading development environment (Rails 3.0.3)
ruby-1.9.2-p136 :001 > d = Dog.first
=> #<Dog id: 1, owner_id: 1, name: "Scooby", secret: "I enjoy crapping everwhere">
ruby-1.9.2-p136 :002 > d.as_json
Dog::as_json
=> {"dog"=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1}}
ruby-1.9.2-p136 :004 > d.owner.as_json(:include => :dog)
Owner::as_json
=> {"owner"=>{"id"=>1, "name"=>"Shaggy", :dog=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1, "secret"=>"I enjoy crapping everwhere"}}}
Thanks for the help
This is a known bug in Rails. (The issue is marked closed due to the migration to Github issues from the previous bug tracker, but it's still a problem as of Rails 3.1.)
As acknowledged above, this is an issue with the Rails base. The rails patch here is not yet applied and seems at least slightly controversial, so I'm hesitant to apply it locally. Even if applied as a monkey patch it could potentially complicate future rails upgrades.
I'm still considering RABL suggested above, it looks useful. For the moment, I'd rather not add another view templating language into my app. My current needs are very small.
So here's a workaround which doesn't require a patch and work for most simple cases. This works where the association's as_json method you'd like to have called looks like
def as_json(options={})
super( <... custom options ...> )
end
In my case I've got Schedule model which has many Events
class Event < ActiveRecord::Base
# define json options as constant, or you could return them from a method
EVENT_JSON_OPTS = { :include => { :locations => { :only => [:id], :methods => [:name] } } }
def as_json(options={})
super(EVENT_JSON_OPTS)
end
end
class Schedule < ActiveRecord::Base
has_many :events
def as_json(options={})
super(:include => { :events => { Event::EVENT_JSON_OPTS } })
end
end
If you followed the guideline that anytime you :include an association in your as_json() methods, you define any options you need as a constant in the model to be referenced, this would work for arbitrary levels of associations. NOTE I only needed the first level of association customized in the above example.
I've found that serializable_hash works just as you'd expect as_json to work, and is always called:
def serializable_hash(options = {})
result = super(options)
result[:url] = "http://.."
result
end
I ran into the same issue. I wanted this to work:
render :json => #favorites.as_json(:include => :location)
But it didn't so I ended up adding an index.json.erb with the following:
<% favs = #favorites.as_json.each do |fav| %>
<% fav["location"] = Location.find(fav["location_id"]).as_json %>
<% end %>
<%= favs.to_json.html_safe %>
Not a fix - just a work around. I imagine you did the same thing.
Update #John pointed out this is a known bug in Rails. A patch to fix it appears to be: at https://github.com/rails/rails/pull/2200. Nevertheless, you might try RABL, because its sweet.
I've always been frustrated with passing a complex set of options to create the JSON views I want. Your problem, which I experienced with Mongoid in Rails 3.0.9, prompted me to write JSON templates. But actually, if you're dealing with relations or custom api properties, it turns out that templates are way nicer.
Besides, dealing with different outputs seems like the View layer to me, so I settled on using RABL, the API templating language. It makes it super easy to build valid JSON and include any associations or fields.
Not a solution to the problem, but a better solution for the use case.
This was reported as a bug: http://ternarylabs.com/2010/09/07/migrating-to-rails-3-0-gotchas-as_json-bug/
I want to be able to replace a field error with a warning when saving/updating a model in rails. Basically I want to just write a wrapper around the validation methods that'll generate the error, save the model and perhaps be available in a warnings hash (which works just like the errors hash):
class Person < ActiveRecord::Base
# normal validation
validates_presence_of :name
# validation with warning
validates_numericality_of :age,
:only_integer => true,
:warning => true # <-- only warn
end
>>> p = Person.new(:name => 'john', :age => 2.2)
>>> p.save
=> true # <-- able to save to db
>>> p.warnings.map { |field, message| "#{field} - #{message}" }
["age - is not a number"] # <-- have access to warning content
Any idea how I could implement this? I was able to add :warning => false default value to ActiveRecord::Validations::ClassMethods::DEFAULT_VALIDATION_OPTIONS
By extending the module, but I'm looking for some insight on how to implement the rest. Thanks.
The validation_scopes gem uses some nice metaprogramming magic to give you all of the usual functionality of validations and ActiveRecord::Errors objects in contexts other than object.errors.
For example, you can say:
validation_scope :warnings do |s|
s.validates_presence_of :some_attr
end
The above validation will be triggered as usual on object.valid?, but won't block saves to the database on object.save if some_attr is not present. Any associated ActiveRecord::Errors objects will be found in object.warnings.
Validations specified in the usual manner without a scope will still behave as expected, blocking database saves and assigning error objects to object.errors.
The author has a brief description of the gem's development on his blog.
I don't know if it's ready for Rails 3, but this plugin does what you are looking for:
http://softvalidations.rubyforge.org/
Edited to add:
To update the basic functionality of this with ActiveModel I came up with the following:
#/config/initializer/soft_validate.rb:
module ActiveRecord
class Base
def warnings
#warnings ||= ActiveModel::Errors.new(self)
end
def complete?
warnings.clear
valid?
warnings.empty?
end
end
end
#/lib/soft_validate_validator.rb
class SoftValidateValidator < ActiveModel::EachValidator
def validate(record)
record.warnings.add_on_blank(attributes, options)
end
end
It adds a new Errors like object called warnings and a helper method complete?, and you can add it to a model like so:
class FollowupReport < ActiveRecord::Base
validates :suggestions, :soft_validate => true
end
I made my own gem to solve the problem for Rails 4.1+: https://github.com/s12chung/active_warnings
class BasicModel
include ActiveWarnings
attr_accessor :name
def initialize(name); #name = name; end
warnings do
validates :name, absence: true
end
end
model = BasicModel.new("some_name")
model.safe? # .invalid? equivalent, but for warnings
model.warnings # .errors equivalent