Rails Nested Attributes JSON Formatting - ruby-on-rails

I'm trying to override the 'as_json' method to include nested attributes for an object, but I'm having trouble properly nesting the JSON.
Currently, I have this in Rails for my 'as_json' method.
// User.rb
def as_json(options = {})
json = {:id => id, :name => name,
:settings_attributes => settings.select(:id,:name),
:setting_options_attributes => setting_options.select(:id, :amount)}
json
end
However, setting_options_attributes should be nested under settings_attributes and I can't find the proper syntax do achieve this.

Assuming settings and settings_options are already hashes, you should be able to do this:
// User.rb
def as_json(options = {})
{
id: id,
name: name,
settings_attributes: settings.select(:id,:name).merge({
setting_options_attributes: setting_options.select(:id, :amount)
},
}
end
If settings and settings_options are Models, then you probably need to do something like this:
// User.rb
def as_json(options = {})
{
id: id,
name: name,
settings_attributes: settings.select(:id,:name).map(&:attributes),
}
end
// Setting.rb
def attributes
{
id: id,
name: name,
setting_options_attributes: setting_options.select(:id,:amount).map(&:attributes),
}
end
It's a little confusing in that SettingOption appears to belong_to User (as you reference it directly) yet you want to nest it within Setting, which implies that it is a "belongs_to :through" relationship, at which point I think you should make Setting responsible for the nesting.
Lastly, if you want to develop complex JSON output, rather than override as_json, you should consider using jbuilder (which is typically bundled with rails) as it moves your JSON logic out of your model and into the view, which is arguably the more appropriate place to be constructing how you view your data. JBuilder is much better at designing complex JSON.

Related

How to modify an attributes of an ActiveRecord instance before render it

In my Rails project, I have a model class Employee < ActiveRecord::Base, the class has an attribute called compensation, it is a :jsonb data type in Postgresql.
create_table "employee" do |t|
...
t.jsonb "compensation"
...
end
The compensation contains salary, working_hours, start_from, and etc
compensation : {
salary: 50000,
working_hours: 230,
start_from: 2018-12-21,
...
}
What I wanna do is that before an instance of Employee being rendered as JSON and response to the front-end, I need to remove the salary attribute in the compensation. In employee_controller I tried to use
def get_without_salary
employee = Employee.find 2
employee.compensation.delete :salary
jsonapi_render json: employee
end
but the result JSON still contains the salary data.
I can only make it works by:
temp_compensation = employee.compensation.dup
temp_compensation.delete :salary
employee.compensation = temp_compensation
but it is too ugly and confused me why the first way failed.
Can someone explain to me why? Thanks
This seems a rather tricky question. The options for as_json are the following:
includes: => used to include has_many and belongs_to associations
methods: => to include the result of a class method of the model
except: => prevent columns from showig
only: => only show specified columns
I tried some of them to specify only some attributes of the jsonb column but they either dont work or throw a NoMethodError: undefined method 'serializable_hash' for #<Hash:0x00000005082eb0> showing that it tries to serialize your jsonb but fails.
I think a clean way of doing it may be to have a method filtered_compensation in your model where you define the json you want to send and prevent compensation from showing.
def get_without_salary
employee = Employee.find 2
jsonapi_render json: employee.as_json(methods: [:filtered_compensation], except: [:compensation])
end
Where filtered_compensation is declared in your model
def filtered_compensation
compensation.except 'salary'
end
I think it complies with being clean, separating concerns and makes your method filtered_compensation to be used elsewhere.
Note that instead of having in your response the attribute compensation, you would find filtered_compensation as that is the name of the method.
After a few days thinking, I came up the solution that first converts the active_record instance into a hash, then delete the salary in the hash, and then converts the hash into an active_record instance. This is a better solution comparing to mutating an object on the fly.
def get_without_salary
employee = Employee.find 2
filtered_employee = filter_salary employee
jsonapi_render json: filtered_employee
end
def filter_salary(employee)
record_hash = employee.as_json.deep_symbolize_keys!
record_hash[:compensation]&.delete :salary
Employee.new(record_hash)
end

Rails custom model setter and querying

Suppose there is a Rails model with a custom setter/accessor and a uniqueness constraint on the name column:
class Person < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
def name=(name)
# Example transformation only.
# Could be substituted for a more complex operation/transformation.
title_cased = name.titleize
self[:name] = title_cased
end
end
Now, consider the following:
Person.create! name: "John Citizen"
Person.find_or_create_by! name: "john citizen" # Error: validation fails
The find operation will not find any results, since there are no entries that match "john citizen". Then, the create! operation will throw an error as there is already an existing entry "John Citizen" (create! creates a new record and raises an exception if the validation fails).
How do you elegantly prevent such errors from occurring? For loose coupling and encapsulation purposes, is it possible to not transform names (to titlecase, in this case) before I perform operations like find_or_create_by! or other operations like find_by?
EDIT:
As #harimohanraj alludes to, the issue seems to be around equivalence. Should the model transparently deal with the understanding/translating input to its boiled-down, canonical state. Or should this be the responsibility of consumers of the class/model?
Also, is active record callbacks a recommended approach to this kind of scenario?
If you have defined a custom setter method, the implicit decision that you have made is: values for the name attribute, no matter what form they come in (eg. a user's input in a text field), should be handled in titleized form in your DB. If that's the case, then it makes sense that find_or_create_by! name: 'john citizen' fails! In other words, your custom setter method represents your decision that "John Citizen" and "john citizen" are one and the same.
If you find yourself wanting to store John Citizen and john citizen in your DB, then I would revisit your decision to create a custom setter method. One cool way to achieve "loose coupling" is to put all of the logic that sanitizes data (ex. data from a user filling out a form) into a separate Ruby object.
There isn't much context in the question, so here is a bit of an abstract example to demonstrate what I mean.
# A class to house the logic of sanitizing your parameters
class PersonParamsSanitizer
# It is initialized with dirty user parameters
def initialize(params)
#params = params
end
# It spits out neat, titleized params
def sanitized_params
{
name: #params[:name].titleize
}
end
end
class PersonController < ApplicationController
def create
# Use your sanitizer object to convert dirty user parameters into neat
# titleized params for your new perons
sanitized_params = UserParamsSanitizer.new(params).sanitized_params
person = Person.new(sanitized_params)
if person.save
redirect_to person
else
render :new
end
end
end
This way, you don't override the setter method in your User model, and are free to use find_or_create_by! fearlessly if you so choose!
You can set a validation to be case-insensitive by using:
class Person < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: { case_sensitive: false }
end
However you also need a case-insensitive database index backing it since just using a validation in Rails will lead to race conditions. How to achieve that depends on the RBDMS.
Which leaves the issue of querying. The classic way of performing a intensive search is by WHERE LOWER(name) = LOWER(?). Although Postgres lets you use WHERE ILIKE name = ?.
If you want to encapsulate this into the model which is a good idea you would create a scope:
class Person
scope :find_by_name, lambda{ |name| where('LOWER(name) = LOWER(?)', name) }
end
However, you cannot use .find_or_create_by! in this case as the query not just a hash. Instead you would call .first_or_create.
Person.find_by_name("John Citizen").first_or_create(attrs)
see also
PostgreSQL: How to make "case-insensitive" query
The problem is the find_or_create_by and similar methods are already not tansforming the name... as you say there is no record "john citizen" but to work properly you'd need to titleize it for the find_or_create_by, find_or_create_by!, or find_by
(you don't need this solution for find as that only retrieves record by primary key)
so...
def self.find_or_create_by(options)
super(rectify_options(options))
end
def self.find_or_create_by!(options)
super(rectify_options(options))
end
def self.find_by(options)
super(rectify_options(options))
end
private
def self.rectify_options(options)
options[:name] = (new.name = options[:name]) if options[:name]
options
end

Present subset of an object with ActiveModel Serializer

I am using ActiveModel Serializers in a Rails project.
The default serializer for the object is fairly large, and nesting an object in API responses result in rather large JSON objects.
Sometimes, I want to embed an object, but only need a small subset of the object's attributes to be present in the JSON.
Obviously, I could do something like this:
render json: #user, serializer: SmallerUserSerializer
but that would lead to a lot of duplication.
Is there an option that I can pass to the serializer so that it will only include a subset of the serializers attributes? Eg:
class BlogSerializer
# This is pseudocode. Does not actually work.
has_one :user, only_show: [:user_id, :profile_url]
end
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
attributes :id, :user
def user
object.user.to_json( only: [ :id, :profile_url ] )
end
end
Use the active model serialzers gem.
Your pseudo code will become the following simple modularized code:
class BlogSerializer < ActiveModel::Serializer
attributes :user_id, :profile_url
end
Guide: http://railscasts.com/episodes/409-active-model-serializers
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
require 'json'
attributes :id, :user
def user
JSON.parse "#{object.user.to_json( only: [ :id, :profile_url ] )}"
end
end

Make ActiveResource models generate hashes for nested objects

In my application, I‘m using ActiveResource to manage the data that I receive from a remote API. Say, one of my models is called Account and it has a field called settings, which is documented in the API as a “freeform hash”, meaning it can store whatever.
A sample simplified JSON I would receive from the API:
{
"account": {
"title": "My Account",
"settings": {
"a_random_setting": true,
"another_random_setting": 42,
"a_subconfig_of_sorts": {
"is_deep": true
}
}
}
}
ActiveResource very kindly goes all the way down the deepest nested objects in that JSON and turns them all into Ruby objects:
my_account.settings # => Account::Settings
my_account.settings.a_subconfig_of_sorts # => Account::Settings::ASubconfigOfSorts
This makes it a bit difficult to look up dynamic keys within that the content of settings. Essentially, I would much rather have settings as regular hash, and not a custom nested object generated for me on the fly.
my_account.settings.class # => Hash
my_account.settings[:a_subconfig_of_sorts] # => {:is_deep => true}
How do I make ActiveResource do that? My first guess was by using the schema declaration, but that only provides scalar types, it seems.
Made solution that works with that issue. Hope that helps.
class Account < ActiveResource::Base
create_reflection :settings_macro, :settings, class_name: 'Account::Hash'
class Hash < ::Hash
def initialize(hash, persisted)
merge!(hash)
end
end
end

Is there a way to rename Strong Parameter keys?

Lets say I had code in a controller that did not use Strong Parameters
Model.create name: params[:name], alias_id: params[:foreign_id]
Is it possible for me to use Strong Parameters here?
I cannot do
Model.create params.require(:name, :foreign_id)
Because foreign_id is not a param
I cannot do
Model.create params.require(:name, :alias_id)
Because alias_id is not on the model.
Basically, I want to know if you can alias paramater keys when using Strong Parameters.
Usually if I want to map params in this way (usually due to some external API) I use the alias_attribute method in ActiveRecord
So if I have params that come to me as {name: "bob", alias_id: 1234} and ideally I would want to have {name: "bob", foreign_id: 1234}
I declare the following in my ActiveRecord model
class MyModel < ActiveRecord::Base
alias_attribute :alias_id, :foreign_id
end
Now my model will always recognise alias_id as mapping to foreign_id in any context and I can push in params.require(:name, :alias_id) and my model will recognise it and process it as required without looping over attributes in my controller.
This is also simpler if you want to do it on other models as well.
I got the functionality I wanted with the following piece of code. I don't think Strong Parameters can do what I need, especially as require() cannot take multiple parameters.
By putting this in my ApplicationController or a module it inherits
#
# Pass in a list of param keys to pull out of the params object
# Alias param keys to another key by specifiying a mapping with a Hash
# eg. filter_params :foo, :bar, {:choo => :zoo}
#
def filter_params(*param_list)
filtered_params = {}
param_list.each do |param|
if param.is_a? Hash
param.each {|key, value| filtered_params[value] = params[key]}
else
filtered_params[param] = params[param]
end
end
filtered_params
end
I can now say
Model.create filter_params(:name, {foreign_id: :alias_id})

Resources