How to override default deserialization of params to model object?
In other words, how to make Rails understand camel case JSON with a snake case database?
Example: I receive params Foo object with a field fooBar and I want my Foo model to understand fooBar is in fact database field foo_bar.
"Foo": {
"fooBar": "hello" /* fooBar is database field foo_bar */
}
class Foo < ActiveRecord::Base
attr_accessible :foo_bar
end
class FoosController < ApplicationController
def new
#foo = Foo.new(params[:foo])
end
Foo.new(params[:foo]) assumes params[:foo] contains foo_bar. Instead params[:foo] contains fooBar (in my case params contains JSON data).
I would like a clean way to handle this case, the same way a model can override as_json:
class Foo < ActiveRecord::Base
attr_accessible :foo_bar, :another_field
def as_json(options = nil)
{
fooBar: foo_bar,
anotherField: another_field
}
end
end
There is a from_json method inside ActiveModel but it is not called when Foo.new(params[:foo]) is run.
I've read several times that overriding initialize from a model object is a terrible idea.
All that Foo.new does with the params hash you give it is iterate over the keys and values in that hash. If the key is foo_bar then it tries to call foo_bar= with the value.
If you define a fooBar= method that sets self.foo_bar then you'll be able to pass a hash with the key :fooBar to Foo.new.
Less manually, you can do
class Foo < ActiveRecord::Base
alias_attribute :fooBar, :foo_bar
end
which generates all the extra accessors for you.
I wouldn't say that overriding initialize is a terrible thing but it can be tricky to do right and there's almost always a simpler way or a way that makes your intentions clearer.
I've checked active_model_serializers, RABL and JBuilder. None of them allow to customize the JSON format that is received.
For that one must deal with wrap_parameters, see http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
It works, still the code is ugly: I get JSON stuff inside my controller + the serializer/model instead of one place.
Example of use of wrap_parameters:
class EventsController < ApplicationController
wrap_parameters :event, include: [:title, :start, :end, :allDay, :description, :location, :color]
def create
respond_with Event.create(params[:event])
end
end
and then inside my model (Frederick Cheung is right on this part):
class Event < ActiveRecord::Base
attr_accessible :title, :start, :end, :allDay, :description, :location, :color
# JSON input allDay is all_day
alias_attribute :allDay, :all_day
# JSON input start is starts_at
# +datetime+:: UNIX time
def start=(datetime)
self.starts_at = Time.at(datetime)
end
# JSON input end is starts_at
# +datetime+:: UNIX time
def end=(datetime)
self.ends_at = Time.at(datetime)
end
# Override the JSON that is returned
def as_json(options = nil)
{
id: id,
title: title,
start: starts_at, # ISO 8601, ex: "2011-10-28T01:22:00Z"
end: ends_at,
allDay: all_day,
description: description, # Not rendered by FullCalendar
location: location,
color: color
}
end
end
For info ASP.NET MVC (with Json.NET) does it using C# decorator attributes which is pretty elegant:
class Post
{
[JsonPropertyAttribute("title")]
public string Title;
}
I have created a gist that shows how to implement serialization/deserialization: https://gist.github.com/3858908
Related
I had a model which returns some parameters and includes parameters from other models as follows:
def as_json(options = {})
camelize_keys(super(options.merge(:only => [:id, :userId], include:{
comments: { only: [:test, :id] },
valediction: { only: [:name, :text, :hidden, :order] }
})))
end
def camelize_keys(hash)
values = hash.map do |key, value|
[key.camelize(:lower), value]
end
Hash[values]
end
Now I have moved the code to my controller because different controller actions need to return different parts of the model. (index should just return valediction, but show should return comments and valediction)
The new controller:
def index
respond_with(displayed_user.microposts.all, include: {
valediction: { only: [:name, :text] }
})
end
def show
respond_with(displayed_user.microposts.find(params[:id]), include: {
comments: { only: [:test, :id] },
valediction: { only: [:name, :text, :hidden, :order] }
})
end
But I'm very new to rails and I don't know how to put the camelize_keys function in so that it works.
Doing complex JSON formatting in your controllers / and or models usually leads to bloat and is a pain to test.
A good solution for this is using the ActiveModel::Serializer (AMS) gem. Its included in Rails 5 but you can easily add it to a Rails 4 project by adding it to the gemfile:
# See rubygems.org for latest verstion!
gem 'active_model_serializers', '~> 0.9.3'
Then run bundle install and restart your rails server.
With AMS you create serializer classes which define how your model data should be represented in JSON, XML etc. A serializer is basically a class that takes a model instance (or an array of models) and returns a hash (or an array of hashes) when you call .serializable_hash.
But Rails will take care of that part automatically for you.
class MicropostSerializer < ActiveModel::Serializer
attributes :id, :user_id
has_many :comments
has_many :valedictions
end
class CommentSerializer < ActiveModel::Serializer
attributes :test, :id
end
class ValedictionSerializer < ActiveModel::Serializer
attributes :name, :text, :hidden, :order
end
In your controller you can simply call:
def index
render json: displayed_user.microposts.all
end
But wait, what about camelize_keys?
Unless you have to support some weird legacy client that needs camelized keys there are very few reasons to do this. Most large API's use snakecase (Facebook, Google etc.) and Rails 5 is moving towards the JSONAPI spec which uses snakecase.
From your code sample it seems that some of your rails model attributes (and the db columns backing them) use camelcase. You should change the DB column with a migration as soon as possible.
If you HAVE to support a legacy database you can use alias_attribute:
class Pet < ActiveRecord::Base
alias_attribute :ownerId, :owner_id
end
You could move the method to a class method in the model, eg
#class methods
class << self
def camelize_keys(hash)
values = hash.map do |key, value|
[key.camelize(:lower), value]
end
Hash[values]
end
end
Now you can call this from anywhere like
MyModel.camelize_keys(some_hash)
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
What is the correct way to do the following in Rails 4? I have a form that takes in a string and some other parameters for a particular model, let's say Foo. This model has an association to another model,
class Foo < ActiveRecord::Base
belongs_to :bar
end
I want to create a new Bar from the given string, assuming a Bar with that name doesn't already exist in the database.
My initial approach was to do this at the controller level, essentially grabbing the name from the params, and then attempting to create a object from it and mutate the original params to include the new object instead of the original string. But I'm realizing that that's not very idiomatic Rails and that I should instead be doing this entirely at the model level.
So how would I accomplish what I want to do at the model level? I am thinking I need a transient attribute and some kind of before_validation filter, but I am not very familiar with Rails. Any ideas?
Not sure to understand correctly what are you trying to do but you might want to take a look at rails nested attributes.
You would have a Foo class like the following:
class Foo < ActiveRecord::Base
has_one :bar
accepts_nested_attributes_for :bar
end
class Bar < ActiveRecord::Base
belongs_to :foo
end
And you could create a new Bar instance directly associated to Foo like so:
foo = Foo.new(bar_attributes: { name: "Bar's name" })
foo.bar.name
#=> "Bar's name"
Why do you need to mutate the params? IMO this should be done at the controller level but without changing any params at all.
class FoosController
before_filter :set_foo
before_filter :set bar
def update
#foo.bar = #bar
#foo.update(foo_params)
respond_with #foo
end
private
def set_foo
#foo = Foo.find(params[:id])
end
def set_bar
#bar = # create/find your Bar from the params here
end
def foo_params
params.require(:foo).permit(...)
end
end
I have a model with some attributes and a virtual attribute.
This virtual attribute is used to make a checkbox in the creation form.
class Thing < ActiveRecord::Base
attr_accessor :foo
attr_accessible :foo
end
Since the field is a checkbox in the form, the foo attribute will receive '0' or '1' as value. I would like it to be a boolean because of the following code:
class Thing < ActiveRecord::Base
attr_accessor :foo
attr_accessible :foo
before_validation :set_default_bar
private
def set_default_bar
self.bar = 'Hello' if foo
end
end
The problem here is that the condition will be true even when foo is '0'. I would like to use the ActiveRecord type casting mechanism but the only I found to do it is the following:
class Thing < ActiveRecord::Base
attr_reader :foo
attr_accessible :foo
before_validation :set_default_bar
def foo=(value)
#foo = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
private
def set_default_bar
self.bar = 'Hello' if foo
end
end
But I feel dirty doing it that way. Is there a better alternative without re-writing the conversion method ?
Thanks
Your solution from the original post looks like the best solution to me.
class Thing < ActiveRecord::Base
attr_reader :foo
def foo=(value)
#foo = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
end
If you wanted to clean things up a bit, you could always create a helper method that defines your foo= writer method for you using value_to_boolean.
I would probably create a module with a method called bool_attr_accessor so you could simplify your model to look like this:
class Thing < ActiveRecord::Base
bool_attr_accessor :foo
end
It seems like ActiveModel ought provide something like this for us, so that virtual attributes act more like "real" (ActiveRecord-persisted) attributes. This type cast is essential whenever you have a boolean virtual attribute that gets submitted from a form.
Maybe we should submit a patch to Rails...
In Rails 5 you can use attribute method. This method defines an attribute with a type on this model. It will override the type of existing attributes if needed.
class Thing < ActiveRecord::Base
attribute :foo, :boolean
end
Caution: there is incorrect behaviour of this attribute feature in rails 5.0.0 on models loaded from the db. Therefore use rails 5.0.1 or higher.
Look at validates_acceptance_of code (click Show source).
They implemented it with comparing to "0".
I'm using it in registrations forms in this way:
class User < ActiveRecord::Base
validates_acceptance_of :terms_of_service
attr_accessible :terms_of_service
end
If you really want cast from string etc you can use this:
def foo=(value)
self.foo=(value == true || value==1 || value =~ (/(true|t|yes|y|1)$/i)) ? true:false
end
Or add typecast method for String class and use it in model:
class String
def to_bool
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
end
end
Why you don't do this :
def foo=(value)
#foo = value
#bar = 'Hello' if value == "1"
end
Im trying set the single table inheritance model type in a form. So i have a select menu for attribute :type and the values are the names of the STI subclasses. The problem is the error log keeps printing:
WARNING: Can't mass-assign these protected attributes: type
So i added "attr_accessible :type" to the model:
class ContentItem < ActiveRecord::Base
# needed so we can set/update :type in mass
attr_accessible :position, :description, :type, :url, :youtube_id, :start_time, :end_time
validates_presence_of :position
belongs_to :chapter
has_many :user_content_items
end
Doesn't change anything, the ContentItem still has :type=nil after .update_attributes() is called in the controller. Any idea how to mass update the :type from a form?
we can override attributes_protected_by_default
class Example < ActiveRecord::Base
def self.attributes_protected_by_default
# default is ["id","type"]
["id"]
end
end
e = Example.new(:type=>"my_type")
You should use the proper constructor based on the subclass you want to create, instead of calling the superclass constructor and assigning type manually. Let ActiveRecord do this for you:
# in controller
def create
# assuming your select has a name of 'content_item_type'
params[:content_item_type].constantize.new(params[:content_item])
end
This gives you the benefits of defining different behavior in your subclasses initialize() method or callbacks. If you don't need these sorts of benefits or are planning to change the class of an object frequently, you may want to reconsider using inheritance and just stick with an attribute.
Duplex at railsforum.com found a workaround:
use a virtual attribute in the forms
and in the model instead of type
dirtectly:
def type_helper
self.type
end
def type_helper=(type)
self.type = type
end
Worked like a charm.
"type" sometimes causes troubles... I usually use "kind" instead.
See also: http://wiki.rubyonrails.org/rails/pages/ReservedWords
I followed http://coderrr.wordpress.com/2008/04/22/building-the-right-class-with-sti-in-rails/ for solving the same problem I had. I'm fairly new to Rails world so am not so sure if this approach is good or bad, but it works very well. I've copied the code below.
class GenericClass < ActiveRecord::Base
class << self
def new_with_cast(*a, &b)
if (h = a.first).is_a? Hash and (type = h[:type] || h['type']) and (klass = type.constantize) != self
raise "wtF hax!!" unless klass < self # klass should be a descendant of us
return klass.new(*a, &b)
end
new_without_cast(*a, &b)
end
alias_method_chain :new, :cast
end
class X < GenericClass; end
GenericClass.new(:type => 'X') # => #<X:0xb79e89d4 #attrs={:type=>"X"}>